forked from assaf/zombie
/
window.coffee
427 lines (351 loc) · 12.9 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
# Create and return a new Window object.
#
# Also responsible for creating associated document and loading it.
Console = require("./console")
EventSource = require("eventsource")
History = require("./history")
JSDOM = require("jsdom")
WebSocket = require("ws")
Scripts = require("./scripts")
URL = require("url")
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
# data - 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)
# url - Set document location to this URL upon opening
createWindow = ({ browser, data, encoding, history, method, name, opener, parent, 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)
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: new Console(browser)
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
browser._cookies.extend(window)
browser._storages.extend(window)
browser._interact.extend(window)
browser._xhr.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 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
return unless document # iframe not loaded
# 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.
event.source = inContext
origin = event.source.location
event.origin = URL.format(protocol: origin.protocol, host: origin.host)
window._dispatchEvent(window, event)
# -- JavaScript evaluation
# Evaulate in context of window. This can be called with a script (String) or a function.
window._evaluate = (code, filename)->
try
inContext = window # the current window, postMessage needs this
if typeof(code) == "string" || code instanceof String
result = global.run(code, filename)
else if code
result = code.call(global)
browser.emit("evaluated", code, result)
return result
catch error
browser.emit("error", error)
finally
inContext = null
window._dispatchEvent = (target, event)->
preventDefault = null
window._evaluate ->
preventDefault = target.dispatchEvent(event)
return preventDefault
# 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.open(name: name, url: url, opener: window)
# Indicates if window was closed
Object.defineProperty window, "closed",
get: -> closed
enumerable: true
# Cleanup the window, child windows and Contextify global.
window._cleanup = ->
unless closed
for frame in window
frame.close()
closed = true
eventQueue.destroy()
window.dispose()
window.document = null
return
# Actualy window.close checks who's attempting to close the window.
window.close = ->
# 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
unless closed
browser.emit("inactive", window)
window._cleanup()
browser.emit("closed", window)
history.dispose() # do this last to prevent infinite loop
else
browser.log("Scripts may not close windows that were not opened by script")
return
# Window is now open, next load the document.
browser.emit("opened", window)
# -- 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)
Object.defineProperties windowHistory,
length:
get: -> return history.length
enumerable: true
state:
get: -> return history.state
enumerable: true
Object.defineProperties window,
history:
value: windowHistory
# Form submission uses this
window._submit = history.submit.bind(history)
# Load the document associated with this window.
loadDocument document: document, history: history, url: url, method: method, encoding: encoding, data: data
return window
# Create an empty document. Each window gets a new document.
createDocument = (browser, window)->
# Create new DOM Level 3 document, add features (load external resources,
# etc) and associate it with current document. From this point on the browser
# sees a new document, client register event handler for
# DOMContentLoaded/error.
jsdom_opts =
deferClose: true
features:
MutationEvents: "2.0"
ProcessExternalResources: []
FetchExternalResources: ["iframe"]
parser: browser.htmlParser
if browser.runScripts
jsdom_opts.features.ProcessExternalResources.push("script")
jsdom_opts.features.FetchExternalResources.push("script")
if browser.loadCSS
jsdom_opts.features.FetchExternalResources.push("css")
document = JSDOM.jsdom(null, HTML, jsdom_opts)
# Add support for running in-line scripts
if browser.runScripts
Scripts.addInlineScriptSupport(document)
# Tie document and window together
Object.defineProperty document, "window",
value: window
Object.defineProperty document, "parentWindow",
value: window.parent # JSDOM property?
Object.defineProperty document, "location",
get: ->
return window.location
set: (url)->
window.location = url
Object.defineProperty document, "URL",
get: ->
return window.location.href
return document
# Load document. Also used to submit form.
loadDocument = ({ document, history, url, method, encoding, data })->
window = document.window
browser = window.browser
referer = history.url || browser.referer
# 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:"
done()
when "javascript:"
try
window._evaluate(pathname, "javascript:")
done()
catch error
done(error)
when "http:", "https:", "file:"
# Proceeed to load resource ...
request =
url: url
method: (method || "GET").toUpperCase()
headers: (headers || {})
data: data
if referer
request.headers.referer = referer
window._eventQueue.http request, (error, response)->
if error
document.open()
document.write(error.message || error)
document.close()
done(error)
return
browser.response = [response.statusCode, response.headers, response.body]
# For responses that contain a non-empty body, load it. Otherwise, we
# already have an empty document in there courtesy of JSDOM.
if response.body
document.open()
document.write(response.body)
document.close()
# Document has loaded
ready = document.createEvent("HTMLEvents")
ready.initEvent("DOMContentLoaded", true, false)
window._dispatchEvent(document, ready)
window._dispatchEvent(window, ready)
onload = document.createEvent("HTMLEvents")
onload.initEvent("load", true, false)
window._dispatchEvent(document, onload)
window._dispatchEvent(window, onload)
# 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"))
# 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