forked from assaf/zombie
-
Notifications
You must be signed in to change notification settings - Fork 0
/
window.coffee
430 lines (359 loc) · 13.5 KB
/
window.coffee
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
#
createDocument = require("./document")
EventSource = require("eventsource")
History = require("./history")
JSDOM = require("jsdom")
WebSocket = require("ws")
URL = require("url")
XMLHttpRequest = require("./xhr")
Events = JSDOM.dom.level3.events
HTML = JSDOM.dom.level3.html
# The current window in context. Set during _evaluate, used by postMessage.
inContext = null
# Create and return a new Window.
#
# Parameters
# browser - Browser that owns this window
# params - Data to submit (used by forms)
# encoding - Encoding MIME type (used by forms)
# history - This window shares history with other windows
# method - HTTP method (used by forms)
# name - Window name (optional)
# opener - Opening window (window.open call)
# parent - Parent window (for frames)
# referer - Use this as referer
# url - Set document location to this URL upon opening
createWindow = ({ browser, params, encoding, history, method, name, opener, parent, referer, url })->
name ||= ""
url ||= "about:blank"
window = JSDOM.createWindow(HTML)
global = window.getGlobal()
# window`s have a closed property defaulting to false
closed = false
# Access to browser
Object.defineProperty window, "browser",
value: browser
enumerable: true
# -- Document --
# Each window has its own document
document = createDocument(browser, window, referer || history.url)
Object.defineProperty window, "document",
value: document
enumerable: true
# -- DOM Window features
Object.defineProperty window, "name",
value: name
enumerable: true
# If this is an iframe within a parent window
if parent
Object.defineProperty window, "parent",
value: parent
enumerable: true
Object.defineProperty window, "top",
value: parent.top
enumerable: true
else
Object.defineProperty window, "parent",
value: global
enumerable: true
Object.defineProperty window, "top",
value: global
enumerable: true
# If this was opened from another window
Object.defineProperty window, "opener",
value: opener && opener
enumerable: true
# Window title is same as document title
Object.defineProperty window, "title",
get: ->
return document.title
set: (title)->
document.title = title
enumerable: true
Object.defineProperty window, "console",
value: browser.console
enumerable: true
# javaEnabled, present in browsers, not in spec Used by Google Analytics see
# https://developer.mozilla.org/en/DOM/window.navigator.javaEnabled
Object.defineProperties window.navigator,
cookieEnabled: { value: true }
javaEnabled: { value: -> false }
plugins: { value: [] }
userAgent: { value: browser.userAgent }
vendor: { value: "Zombie Industries" }
# Add cookies, storage, alerts/confirm, XHR, WebSockets, JSON, Screen, etc
Object.defineProperty window, "cookies",
get: ->
return browser.cookies(@location.hostname, @location.pathname)
browser._storages.extend(window)
browser._interact.extend(window)
Object.defineProperties window,
File: { value: File }
Event: { value: Events.Event }
screen: { value: new Screen() }
MouseEvent: { value: Events.MouseEvent }
MutationEvent: { value: Events.MutationEvent }
UIEvent: { value: Events.UIEvent }
# Base-64 encoding/decoding
window.atob = (string)->
new Buffer(string, "base64").toString("utf8")
window.btoa = (string)->
new Buffer(string, "utf8").toString("base64")
# Constructor for XHLHttpRequest
window.XMLHttpRequest = ->
return new XMLHttpRequest(window)
# Constructor for EventSource, URL is relative to document's.
window.EventSource = (url)->
url = HTML.resourceLoader.resolve(document, url)
window.setInterval((->), 100) # We need this to trigger event loop
return new EventSource(url)
# Web sockets
window.WebSocket = (url, protocol)->
url = HTML.resourceLoader.resolve(document, url)
origin = "#{window.location.protocol}//#{window.location.host}"
return new WebSocket(url, origin: origin, protocol: protocol)
window.Image = (width, height)->
img = new HTML.HTMLImageElement(window.document)
img.width = width
img.height = height
return img
window.resizeTo = (width, height)->
window.outerWidth = window.innerWidth = width
window.outerHeight = window.innerHeight = height
window.resizeBy = (width, height)->
window.resizeTo(window.outerWidth + width, window.outerHeight + height)
# Help iframes talking with each other
window.postMessage = (data, targetOrigin)->
document = window.document
# Create the event now, but dispatch asynchronously
event = document.createEvent("MessageEvent")
event.initEvent("message", false, false)
event.data = data
# Window A (source) calls B.postMessage, to determine A we need the
# caller's window.
# DDOPSON-2012-11-09 - inContext.getGlobal() is used here so that for
# website code executing inside the sandbox context, event.source == window.
# Even though the inContext object is mapped to the sandboxed version of the
# object returned by getGlobal, they are not the same object ie,
# inContext.foo == inContext.getGlobal().foo, but inContext !=
# inContext.getGlobal()
event.source = inContext.getGlobal()
origin = event.source.location
event.origin = URL.format(protocol: origin.protocol, host: origin.host)
window.dispatchEvent(event)
# -- JavaScript evaluation
# Evaulate in context of window. This can be called with a script (String) or a function.
window._evaluate = (code, filename)->
try
# The current window, postMessage and window.close need this
[original, inContext] = [inContext, window]
if typeof(code) == "string" || code instanceof String
result = global.run(code, filename)
else if code
result = code.call(global)
browser.emit("evaluated", code, result, filename)
return result
catch error
error.filename ||= filename
browser.emit("error", error)
finally
inContext = original
# Default onerror handler.
window.onerror = (event)->
error = event.error || new Error("Error loading script")
browser.emit("error", error)
# -- Event loop --
eventQueue = browser.eventLoop.createEventQueue(window)
Object.defineProperties window,
_eventQueue:
value: eventQueue
setTimeout:
value: eventQueue.setTimeout.bind(eventQueue)
clearTimeout:
value: eventQueue.clearTimeout.bind(eventQueue)
setInterval:
value: eventQueue.setInterval.bind(eventQueue)
clearInterval:
value: eventQueue.clearInterval.bind(eventQueue)
# -- Opening and closing --
# Open one window from another.
window.open = (url, name, features)->
url = url && HTML.resourceLoader.resolve(document, url)
return browser.tabs.open(name: name, url: url, opener: window)
# Indicates if window was closed
Object.defineProperty window, "closed",
get: -> closed
enumerable: true
# Destroy all the history (and all its windows), frames, and Contextify
# global.
window._destroy = ->
# We call history.distroy which calls destroy on all windows, so need to
# avoid infinite loop.
unless closed
closed = true
for frame in window.frames
frame.close()
eventQueue.destroy()
window.document = null
window.dispose()
return
# window.close actually closes the tab, and disposes of all windows in the history.
# Also used to close iframe.
window.close = ->
return if parent || closed
# Only opener window can close window; any code that's not running from
# within a window's context can also close window.
if inContext == opener || inContext == null
browser.emit("closed", window)
history.destroy()
window._destroy() # do this last to prevent infinite loop
else
browser.log("Scripts may not close windows that were not opened by script")
return
# -- Navigating --
history.updateLocation(window, url)
# Each window maintains its own view of history
windowHistory =
forward: history.go.bind(history, 1)
back: history.go.bind(history, -1)
go: history.go.bind(history)
pushState: history.pushState.bind(history)
replaceState: history.replaceState.bind(history)
_submit: history.submit.bind(history)
Object.defineProperties windowHistory,
length:
get: -> return history.length
enumerable: true
state:
get: -> return history.state
enumerable: true
Object.defineProperties window,
history:
value: windowHistory
# Window is now open, next load the document.
browser.emit("opened", window)
# Form submission uses this
window._submit = ({url, method, encoding, params, target })->
url = HTML.resourceLoader.resolve(document, url)
target ||= "_self"
browser.emit("submit", url, target)
# Figure out which history is going to handle this
switch target
when "_self" # navigate same window
submitTo = window
when "_parent" # navigate parent window
submitTo = window.parent
when "_top" # navigate top window
submitTo = window.top
else # open named window
submitTo = browser.tabs.open(name: anchor.target)
submitTo.history._submit(url: url, method: method, encoding: encoding, params: params)
# Load the document associated with this window.
loadDocument document: document, history: history, url: url, method: method, encoding: encoding, params: params
return window
# Load document. Also used to submit form.
loadDocument = ({ document, history, url, method, encoding, params })->
window = document.window
browser = window.browser
window._response = { }
# Called on wrap up to update browser with outcome.
done = (error, url)->
if error
browser.emit("error", error)
else
if url
history.updateLocation(window, url)
browser.emit("loaded", document)
method = (method || "GET").toUpperCase()
if method == "POST"
headers =
"content-type": encoding || "application/x-www-form-urlencoded"
# Let's handle the specifics of each protocol
{ protocol, pathname } = URL.parse(url)
switch protocol
when "about:"
document.open()
document.write("<html><body></body></html>")
document.close()
done()
when "javascript:"
try
window._evaluate(pathname, "javascript:")
done()
catch error
done(error)
when "http:", "https:", "file:"
# Proceeed to load resource ...
headers = headers || {}
unless headers.referer
# HTTP header Referer, but Document property referrer
headers.referer = document.referrer
window._eventQueue.http method, url, headers: headers, params: params, target: document, (error, response)->
if error
document.close()
document.write(error.message || error)
document.close()
done(error)
return
window._response = response
# JSDOM fires load event on document but not on window
windowLoaded = (event)->
document.removeEventListener("load", windowLoaded)
window.dispatchEvent(event)
document.addEventListener("load", windowLoaded)
# JSDOM fires load event on document but not on window
contentLoaded = (event)->
document.removeEventListener("DOMContentLoaded", contentLoaded)
window.dispatchEvent(event)
document.addEventListener("DOMContentLoaded", contentLoaded)
# Give event handler chance to register listeners.
window.browser.emit("loading", document)
if response.body
body = response.body.toString("utf8")
else
body = "<html><body></body></html>"
document.open()
document.write(body)
document.close()
# Error on any response that's not 2xx, or if we're not smart enough to
# process the content and generate an HTML DOM tree from it.
if response.statusCode >= 400
done(new Error("Server returned status code #{response.statusCode} from #{url}"))
else if document.documentElement
done(null, response.url)
else
done(new Error("Could not parse document at #{url}"))
else # but not any other protocol for now
done(new Error("Cannot load resource #{url}, unsupported protocol"))
# Wrap dispatchEvent to support inContext and error handling.
jsdomDispatchElement = HTML.Element.prototype.dispatchEvent
HTML.Node.prototype.dispatchEvent = (event)->
self = this
# Could be node, window or document
document = self.ownerDocument || self.document || self
window = document.window
window.browser.emit("event", event, self)
try
# The current window, postMessage and window.close need this
[original, inContext] = [inContext, window]
return jsdomDispatchElement.call(self, event)
catch error
error.filename ||= filename
browser.emit("error", error)
finally
inContext = original
# Screen object provides access to screen dimensions
class Screen
constructor: ->
@top = @left = 0
@width = 1280
@height = 800
@prototype.__defineGetter__ "availLeft", -> 0
@prototype.__defineGetter__ "availTop", -> 0
@prototype.__defineGetter__ "availWidth", -> 1280
@prototype.__defineGetter__ "availHeight", -> 800
@prototype.__defineGetter__ "colorDepth", -> 24
@prototype.__defineGetter__ "pixelDepth", -> 24
# File access, not implemented yet
class File
module.exports = createWindow