forked from jmoenig/Snap
-
Notifications
You must be signed in to change notification settings - Fork 6
/
iframe-phone.js
466 lines (399 loc) · 15.7 KB
/
iframe-phone.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.iframePhone=e():"undefined"!=typeof global?global.iframePhone=e():"undefined"!=typeof self&&(self.iframePhone=e())}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var structuredClone = require('./structured-clone');
var HELLO_INTERVAL_LENGTH = 200;
var HELLO_TIMEOUT_LENGTH = 60000;
function IFrameEndpoint() {
var listeners = {};
var isInitialized = false;
var connected = false;
var postMessageQueue = [];
var helloInterval;
function postToParent(message) {
// See http://dev.opera.com/articles/view/window-postmessage-messagechannel/#crossdoc
// https://github.com/Modernizr/Modernizr/issues/388
// http://jsfiddle.net/ryanseddon/uZTgD/2/
if (structuredClone.supported()) {
window.parent.postMessage(message, '*');
} else {
window.parent.postMessage(JSON.stringify(message), '*');
}
}
function post(type, content) {
var message;
// Message object can be constructed from 'type' and 'content' arguments or it can be passed
// as the first argument.
if (arguments.length === 1 && typeof type === 'object' && typeof type.type === 'string') {
message = type;
} else {
message = {
type: type,
content: content
};
}
if (connected) {
postToParent(message);
} else {
postMessageQueue.push(message);
}
}
function postHello() {
postToParent({
type: 'hello'
});
}
function addListener(type, fn) {
listeners[type] = fn;
}
function removeAllListeners() {
listeners = {};
}
function getListenerNames() {
return Object.keys(listeners);
}
function messageListener(message) {
// Anyone can send us a message. Only pay attention to messages from parent.
if (message.source !== window.parent) return;
var messageData = message.data;
if (typeof messageData === 'string') messageData = JSON.parse(messageData);
if (!connected && messageData.type === 'hello') {
connected = true;
stopPostingHello();
while (postMessageQueue.length > 0) {
post(postMessageQueue.shift());
}
}
if (connected && listeners[messageData.type]) {
listeners[messageData.type](messageData.content);
}
}
function disconnect() {
connected = false;
stopPostingHello();
window.removeEventListener('message', messageListener);
}
/**
Initialize communication with the parent frame. This should not be called until the app's custom
listeners are registered (via our 'addListener' public method) because, once we open the
communication, the parent window may send any messages it may have queued. Messages for which
we don't have handlers will be silently ignored.
*/
function initialize() {
if (isInitialized) {
return;
}
isInitialized = true;
if (window.parent === window) return;
// We kick off communication with the parent window by sending a "hello" message. Then we wait
// for a handshake (another "hello" message) from the parent window.
startPostingHello();
window.addEventListener('message', messageListener, false);
}
function startPostingHello() {
if (helloInterval) {
stopPostingHello();
}
helloInterval = window.setInterval(postHello, HELLO_INTERVAL_LENGTH);
window.setTimeout(stopPostingHello, HELLO_TIMEOUT_LENGTH);
// Post the first msg immediately.
postHello();
}
function stopPostingHello() {
window.clearInterval(helloInterval);
helloInterval = null;
}
// Public API.
return {
initialize: initialize,
getListenerNames: getListenerNames,
addListener: addListener,
removeAllListeners: removeAllListeners,
disconnect: disconnect,
post: post
};
}
var instance = null;
// IFrameEndpoint is a singleton, as iframe can't have multiple parents anyway.
module.exports = function getIFrameEndpoint() {
if (!instance) {
instance = new IFrameEndpoint();
}
return instance;
};
},{"./structured-clone":4}],2:[function(require,module,exports){
var ParentEndpoint = require('./parent-endpoint');
var getIFrameEndpoint = require('./iframe-endpoint');
// Not a real UUID as there's an RFC for that (needed for proper distributed computing).
// But in this fairly parochial situation, we just need to be fairly sure to avoid repeats.
function getPseudoUUID() {
var chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
var len = chars.length;
var ret = [];
for (var i = 0; i < 10; i++) {
ret.push(chars[Math.floor(Math.random() * len)]);
}
return ret.join('');
}
module.exports = function IframePhoneRpcEndpoint(handler, namespace, targetWindow, targetOrigin, phone) {
var pendingCallbacks = Object.create({});
// if it's a non-null object, rather than a function, 'handler' is really an options object
if (handler && typeof handler === 'object') {
namespace = handler.namespace;
targetWindow = handler.targetWindow;
targetOrigin = handler.targetOrigin;
phone = handler.phone;
handler = handler.handler;
}
if (!phone) {
if (targetWindow === window.parent) {
phone = getIFrameEndpoint();
phone.initialize();
} else {
phone = new ParentEndpoint(targetWindow, targetOrigin);
}
}
phone.addListener(namespace, function (message) {
var callbackObj;
if (message.messageType === 'call' && typeof this.handler === 'function') {
this.handler.call(undefined, message.value, function (returnValue) {
phone.post(namespace, {
messageType: 'returnValue',
uuid: message.uuid,
value: returnValue
});
});
} else if (message.messageType === 'returnValue') {
callbackObj = pendingCallbacks[message.uuid];
if (callbackObj) {
window.clearTimeout(callbackObj.timeout);
if (callbackObj.callback) {
callbackObj.callback.call(undefined, message.value);
}
pendingCallbacks[message.uuid] = null;
}
}
}.bind(this));
function call(message, callback) {
var uuid = getPseudoUUID();
pendingCallbacks[uuid] = {
callback: callback,
timeout: window.setTimeout(function () {
if (callback) {
callback(undefined, new Error("IframePhone timed out waiting for reply"));
}
}, 2000)
};
phone.post(namespace, {
messageType: 'call',
uuid: uuid,
value: message
});
}
function disconnect() {
phone.disconnect();
}
this.handler = handler;
this.call = call.bind(this);
this.disconnect = disconnect.bind(this);
};
},{"./iframe-endpoint":1,"./parent-endpoint":3}],3:[function(require,module,exports){
var structuredClone = require('./structured-clone');
/**
Call as:
new ParentEndpoint(targetWindow, targetOrigin, afterConnectedCallback)
targetWindow is a WindowProxy object. (Messages will be sent to it)
targetOrigin is the origin of the targetWindow. (Messages will be restricted to this origin)
afterConnectedCallback is an optional callback function to be called when the connection is
established.
OR (less secure):
new ParentEndpoint(targetIframe, afterConnectedCallback)
targetIframe is a DOM object (HTMLIframeElement); messages will be sent to its contentWindow.
afterConnectedCallback is an optional callback function
In this latter case, targetOrigin will be inferred from the value of the src attribute of the
provided DOM object at the time of the constructor invocation. This is less secure because the
iframe might have been navigated to an unexpected domain before constructor invocation.
Note that it is important to specify the expected origin of the iframe's content to safeguard
against sending messages to an unexpected domain. This might happen if our iframe is navigated to
a third-party URL unexpectedly. Furthermore, having a reference to Window object (as in the first
form of the constructor) does not protect against sending a message to the wrong domain. The
window object is actually a WindowProxy which transparently proxies the Window object of the
underlying iframe, so that when the iframe is navigated, the "same" WindowProxy now references a
completely different Window object, possibly controlled by a hostile domain.
See http://www.esdiscuss.org/topic/a-dom-use-case-that-can-t-be-emulated-with-direct-proxies for
more about this weird behavior of WindowProxies (the type returned by <iframe>.contentWindow).
*/
module.exports = function ParentEndpoint(targetWindowOrIframeEl, targetOrigin, afterConnectedCallback) {
var postMessageQueue = [];
var connected = false;
var handlers = {};
var targetWindowIsIframeElement;
function getIframeOrigin(iframe) {
return iframe.src.match(/(.*?\/\/.*?)\//)[1];
}
function post(type, content) {
var message;
// Message object can be constructed from 'type' and 'content' arguments or it can be passed
// as the first argument.
if (arguments.length === 1 && typeof type === 'object' && typeof type.type === 'string') {
message = type;
} else {
message = {
type: type,
content: content
};
}
if (connected) {
var tWindow = getTargetWindow();
// if we are already connected ... send the message
// See http://dev.opera.com/articles/view/window-postmessage-messagechannel/#crossdoc
// https://github.com/Modernizr/Modernizr/issues/388
// http://jsfiddle.net/ryanseddon/uZTgD/2/
if (structuredClone.supported()) {
tWindow.postMessage(message, targetOrigin);
} else {
tWindow.postMessage(JSON.stringify(message), targetOrigin);
}
} else {
// else queue up the messages to send after connection complete.
postMessageQueue.push(message);
}
}
function addListener(messageName, func) {
handlers[messageName] = func;
}
function removeListener(messageName) {
handlers[messageName] = null;
}
// Note that this function can't be used when IFrame element hasn't been added to DOM yet
// (.contentWindow would be null). At the moment risk is purely theoretical, as the parent endpoint
// only listens for an incoming 'hello' message and the first time we call this function
// is in #receiveMessage handler (so iframe had to be initialized before, as it could send 'hello').
// It would become important when we decide to refactor the way how communication is initialized.
function getTargetWindow() {
if (targetWindowIsIframeElement) {
var tWindow = targetWindowOrIframeEl.contentWindow;
if (!tWindow) {
throw "IFrame element needs to be added to DOM before communication " +
"can be started (.contentWindow is not available)";
}
return tWindow;
}
return targetWindowOrIframeEl;
}
function receiveMessage(message) {
var messageData;
if (message.source === getTargetWindow() && (targetOrigin === '*' || message.origin === targetOrigin)) {
messageData = message.data;
if (typeof messageData === 'string') {
messageData = JSON.parse(messageData);
}
if (handlers[messageData.type]) {
handlers[messageData.type](messageData.content);
} else {
console.log("cant handle type: " + messageData.type);
}
}
}
function disconnect() {
connected = false;
window.removeEventListener('message', receiveMessage);
}
// handle the case that targetWindowOrIframeEl is actually an <iframe> rather than a Window(Proxy) object
// Note that if it *is* a WindowProxy, this probe will throw a SecurityException, but in that case
// we also don't need to do anything
try {
targetWindowIsIframeElement = targetWindowOrIframeEl.constructor === HTMLIFrameElement;
} catch (e) {
targetWindowIsIframeElement = false;
}
if (targetWindowIsIframeElement) {
// Infer the origin ONLY if the user did not supply an explicit origin, i.e., if the second
// argument is empty or is actually a callback (meaning it is supposed to be the
// afterConnectionCallback)
if (!targetOrigin || targetOrigin.constructor === Function) {
afterConnectedCallback = targetOrigin;
targetOrigin = getIframeOrigin(targetWindowOrIframeEl);
}
}
// Handle pages served through file:// protocol. Behavior varies in different browsers. Safari sets origin
// to 'file://' and everything works fine, but Chrome and Safari set message.origin to null.
// Also, https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage says:
// > Lastly, posting a message to a page at a file: URL currently requires that the targetOrigin argument be "*".
// > file:// cannot be used as a security restriction; this restriction may be modified in the future.
// So, using '*' seems like the only possible solution.
if (targetOrigin === 'file://') {
targetOrigin = '*';
}
// when we receive 'hello':
addListener('hello', function () {
connected = true;
// send hello response
post({
type: 'hello',
// `origin` property isn't used by IframeEndpoint anymore (>= 1.2.0), but it's being sent to be
// backward compatible with old IframeEndpoint versions (< v1.2.0).
origin: window.location.href.match(/(.*?\/\/.*?)\//)[1]
});
// give the user a chance to do things now that we are connected
// note that is will happen before any queued messages
if (afterConnectedCallback && typeof afterConnectedCallback === "function") {
afterConnectedCallback();
}
// Now send any messages that have been queued up ...
while (postMessageQueue.length > 0) {
post(postMessageQueue.shift());
}
});
window.addEventListener('message', receiveMessage, false);
// Public API.
return {
post: post,
addListener: addListener,
removeListener: removeListener,
disconnect: disconnect,
getTargetWindow: getTargetWindow,
targetOrigin: targetOrigin
};
};
},{"./structured-clone":4}],4:[function(require,module,exports){
var featureSupported = {
'structuredClones': 0
};
(function () {
var result = 0;
if (!!window.postMessage) {
try {
// Spec states you can't transmit DOM nodes and it will throw an error
// postMessage implementations that support cloned data will throw.
window.postMessage(document.createElement("a"), "*");
} catch (e) {
// BBOS6 throws but doesn't pass through the correct exception
// so check error message
result = (e.DATA_CLONE_ERR || e.message === "Cannot post cyclic structures.") ? 1 : 0;
featureSupported = {
'structuredClones': result
};
}
}
}());
exports.supported = function supported() {
return featureSupported && featureSupported.structuredClones > 0;
};
},{}],5:[function(require,module,exports){
module.exports = {
/**
* Allows to communicate with an iframe.
*/
ParentEndpoint: require('./lib/parent-endpoint'),
/**
* Allows to communicate with a parent page.
* IFrameEndpoint is a singleton, as iframe can't have multiple parents anyway.
*/
getIFrameEndpoint: require('./lib/iframe-endpoint'),
structuredClone: require('./lib/structured-clone'),
// TODO: May be misnamed
IframePhoneRpcEndpoint: require('./lib/iframe-phone-rpc-endpoint')
};
},{"./lib/iframe-endpoint":1,"./lib/iframe-phone-rpc-endpoint":2,"./lib/parent-endpoint":3,"./lib/structured-clone":4}]},{},[5])
(5)
});
;