Web Inspector: document Debugger.paused, introduce types for ids in Debugger domain.
[WebKit.git] / Source / WebCore / inspector / InjectedScriptSource.js
1 /*
2  * Copyright (C) 2007 Apple Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer.
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution.
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 (function (InjectedScriptHost, inspectedWindow, injectedScriptId) {
30
31 function bind(thisObject, memberFunction)
32 {
33     var func = memberFunction;
34     var args = Array.prototype.slice.call(arguments, 2);
35     function bound()
36     {
37         return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0)));
38     }
39     bound.toString = function() {
40         return "bound: " + func;
41     };
42     return bound;
43 }
44
45 var InjectedScript = function()
46 {
47     this._lastBoundObjectId = 1;
48     this._idToWrappedObject = {};
49     this._idToObjectGroupName = {};
50     this._objectGroups = {};
51 }
52
53 InjectedScript.primitiveTypes = {
54     undefined: true,
55     boolean: true,
56     number: true,
57     string: true
58 }
59
60 InjectedScript.prototype = {
61     isPrimitiveValue: function(object)
62     {
63         // FIXME(33716): typeof document.all is always 'undefined'.
64         return InjectedScript.primitiveTypes[typeof object] && !this._isHTMLAllCollection(object);
65     },
66
67     wrapObject: function(object, groupName, canAccessInspectedWindow)
68     {
69         if (canAccessInspectedWindow)
70             return this._wrapObject(object, groupName);
71
72         var result = {};
73         result.type = typeof object;
74         if (this._isPrimitiveValue(object))
75             result.value = object;
76         else
77             result.description = this._toString(object);
78         return result;
79     },
80
81     inspectNode: function(object)
82     {
83         this._inspect(object);
84     },
85
86     _inspect: function(object)
87     {
88         if (arguments.length === 0)
89             return;
90
91         var objectId = this._wrapObject(object, "");
92         var hints = {};
93
94         switch (injectedScript._describe(object)) {
95             case "Database":
96                 var databaseId = InjectedScriptHost.databaseId(object)
97                 if (databaseId)
98                     hints.databaseId = databaseId;
99                 break;
100             case "Storage":
101                 var storageId = InjectedScriptHost.storageId(object)
102                 if (storageId)
103                     hints.domStorageId = storageId;
104                 break;
105         }
106         InjectedScriptHost.inspect(objectId, hints);
107         return object;
108     },
109
110     // This method cannot throw.
111     _wrapObject: function(object, objectGroupName, forceValueType)
112     {
113         try {
114             return new InjectedScript.RemoteObject(object, objectGroupName, forceValueType);
115         } catch (e) {
116             try {
117                 var description = injectedScript._describe(e);
118             } catch (ex) {
119                 var description = "<failed to convert exception to string>";
120             }
121             return new InjectedScript.RemoteObject(description);
122         }
123     },
124
125     _bind: function(object, objectGroupName)
126     {
127         var id = this._lastBoundObjectId++;
128         this._idToWrappedObject[id] = object;
129         var objectId = "{\"injectedScriptId\":" + injectedScriptId + ",\"id\":" + id + "}";
130         if (objectGroupName) {
131             var group = this._objectGroups[objectGroupName];
132             if (!group) {
133                 group = [];
134                 this._objectGroups[objectGroupName] = group;
135             }
136             group.push(id);
137             this._idToObjectGroupName[id] = objectGroupName;
138         }
139         return objectId;
140     },
141
142     _parseObjectId: function(objectId)
143     {
144         return InjectedScriptHost.evaluate("(" + objectId + ")");
145     },
146
147     releaseObjectGroup: function(objectGroupName)
148     {
149         var group = this._objectGroups[objectGroupName];
150         if (!group)
151             return;
152         for (var i = 0; i < group.length; i++)
153             this._releaseObject(group[i]);
154         delete this._objectGroups[objectGroupName];
155     },
156
157     dispatch: function(methodName, args)
158     {
159         var argsArray = InjectedScriptHost.evaluate("(" + args + ")");
160         var result = this[methodName].apply(this, argsArray);
161         if (typeof result === "undefined") {
162             inspectedWindow.console.error("Web Inspector error: InjectedScript.%s returns undefined", methodName);
163             result = null;
164         }
165         return result;
166     },
167
168     getProperties: function(objectId, ownProperties)
169     {
170         var parsedObjectId = this._parseObjectId(objectId);
171         var object = this._objectForId(parsedObjectId);
172         var objectGroupName = this._idToObjectGroupName[parsedObjectId.id];
173
174         if (!this._isDefined(object))
175             return false;
176         var descriptors = this._propertyDescriptors(object, ownProperties);
177
178         // Go over properties, wrap object values.
179         if (descriptors.length === 0 && "arguments" in object) {
180             // Fill in JSC scope object.
181             for (var key in object)
182                 descriptors.push({ name: key, value: object[key], writable: false, configurable: false, enumerable: true});
183         }
184
185         for (var i = 0; i < descriptors.length; ++i) {
186             var descriptor = descriptors[i];
187             if (descriptor.get)
188                 descriptor.get = this._wrapObject(descriptor.get, objectGroupName);
189             if (descriptor.set)
190                 descriptor.set = this._wrapObject(descriptor.set, objectGroupName);
191             if ("value" in descriptor)
192                 descriptor.value = this._wrapObject(descriptor.value, objectGroupName);
193         }
194         return descriptors;
195     },
196
197     releaseObject: function(objectId)
198     {
199         var parsedObjectId = this._parseObjectId(objectId);
200         this._releaseObject(parsedObjectId.id);
201     },
202
203     _releaseObject: function(id)
204     {
205         delete this._idToWrappedObject[id];
206         delete this._idToObjectGroupName[id];
207     },
208
209     _propertyDescriptors: function(object, ownProperties)
210     {
211         var descriptors = [];
212         var nameProcessed = {};
213         nameProcessed.__proto__ = null;
214         for (var o = object; this._isDefined(o); o = o.__proto__) {
215             var names = Object.getOwnPropertyNames(o);
216             for (var i = 0; i < names.length; ++i) {
217                 var name = names[i];
218                 if (nameProcessed[name])
219                     continue;
220
221                 try {
222                     nameProcessed[name] = true;
223                     var descriptor = Object.getOwnPropertyDescriptor(object, name);
224                     if (!descriptor) {
225                         // Not all bindings provide proper descriptors. Fall back to the writable, configurable property.
226                         try {
227                             descriptors.push({ name: name, value: object[name], writable: false, configurable: false, enumerable: false});
228                         } catch (e) {
229                             // Silent catch.
230                         }
231                         continue;
232                     }
233                 } catch (e) {
234                     var descriptor = {};
235                     descriptor.value = e;
236                     descriptor.wasThrown = true;
237                 }
238
239                 descriptor.name = name;
240                 descriptors.push(descriptor); 
241             }
242             if (ownProperties) {
243                 if (object.__proto__)
244                     descriptors.push({ name: "__proto__", value: object.__proto__, writable: true, configurable: true, enumerable: false});
245                 break;
246             }
247         }
248         return descriptors;
249     },
250
251     evaluate: function(expression, objectGroup, injectCommandLineAPI, returnByValue)
252     {
253         return this._evaluateAndWrap(InjectedScriptHost.evaluate, InjectedScriptHost, expression, objectGroup, false, injectCommandLineAPI, returnByValue);
254     },
255
256     callFunctionOn: function(objectId, expression, args, returnByValue)
257     {
258         var parsedObjectId = this._parseObjectId(objectId);
259         var object = this._objectForId(parsedObjectId);
260         if (!object)
261             return "Could not find object with given id";
262
263         if (args) {
264             var resolvedArgs = [];
265             args = InjectedScriptHost.evaluate(args);
266             for (var i = 0; i < args.length; ++i) {
267                 var objectId = args[i].objectId;
268                 if (objectId) {
269                     var parsedArgId = this._parseObjectId(objectId);
270                     if (!parsedArgId || parsedArgId.injectedScriptId !== injectedScriptId)
271                         return "Arguments should belong to the same JavaScript world as the target object.";
272
273                     var resolvedArg = this._objectForId(parsedArgId);
274                     if (!resolvedArg)
275                         return "Could not find object with given id";
276
277                     resolvedArgs.push(resolvedArg);
278                 } else if ("value" in args[i])
279                     resolvedArgs.push(args[i].value);
280                 else
281                     resolvedArgs.push(undefined);
282             }
283         }
284
285         try {
286             var objectGroup = this._idToObjectGroupName[parsedObjectId.id];
287             var func = InjectedScriptHost.evaluate("(" + expression + ")");
288             if (typeof func !== "function")
289                 return "Given expression does not evaluate to a function";
290
291             return { wasThrown: false,
292                      result: this._wrapObject(func.apply(object, resolvedArgs), objectGroup, returnByValue) };
293         } catch (e) {
294             return this._createThrownValue(e, objectGroup);
295         }
296     },
297
298     _evaluateAndWrap: function(evalFunction, object, expression, objectGroup, isEvalOnCallFrame, injectCommandLineAPI, returnByValue)
299     {
300         try {
301             return { wasThrown: false,
302                      result: this._wrapObject(this._evaluateOn(evalFunction, object, expression, isEvalOnCallFrame, injectCommandLineAPI), objectGroup, returnByValue) };
303         } catch (e) {
304             return this._createThrownValue(e, objectGroup);
305         }
306     },
307
308     _createThrownValue: function(value, objectGroup)
309     {
310         var remoteObject = this._wrapObject(value, objectGroup);
311         try {
312             remoteObject.description = this._toString(value);
313         } catch (e) {}
314         return { wasThrown: true,
315                  result: remoteObject };
316     },
317
318     _evaluateOn: function(evalFunction, object, expression, isEvalOnCallFrame, injectCommandLineAPI)
319     {
320         // Only install command line api object for the time of evaluation.
321         // Surround the expression in with statements to inject our command line API so that
322         // the window object properties still take more precedent than our API functions.
323
324         try {
325             if (injectCommandLineAPI && inspectedWindow.console) {
326                 inspectedWindow.console._commandLineAPI = new CommandLineAPI(this._commandLineAPIImpl, isEvalOnCallFrame ? object : null);
327                 expression = "with ((window && window.console && window.console._commandLineAPI) || {}) {\n" + expression + "\n}";
328             }
329             return evalFunction.call(object, expression);
330         } finally {
331             if (injectCommandLineAPI && inspectedWindow.console)
332                 delete inspectedWindow.console._commandLineAPI;
333         }
334     },
335
336     wrapCallFrames: function(callFrame)
337     {
338         if (!callFrame)
339             return false;
340
341         var result = [];
342         var depth = 0;
343         do {
344             result.push(new InjectedScript.CallFrameProxy(depth++, callFrame));
345             callFrame = callFrame.caller;
346         } while (callFrame);
347         return result;
348     },
349
350     evaluateOnCallFrame: function(topCallFrame, callFrameId, expression, objectGroup, injectCommandLineAPI, returnByValue)
351     {
352         var callFrame = this._callFrameForId(topCallFrame, callFrameId);
353         if (!callFrame)
354             return "Could not find call frame with given id";
355         return this._evaluateAndWrap(callFrame.evaluate, callFrame, expression, objectGroup, true, injectCommandLineAPI, returnByValue);
356     },
357
358     _callFrameForId: function(topCallFrame, callFrameId)
359     {
360         var parsedCallFrameId = InjectedScriptHost.evaluate("(" + callFrameId + ")");
361         var ordinal = parsedCallFrameId.ordinal;
362         var callFrame = topCallFrame;
363         while (--ordinal >= 0 && callFrame)
364             callFrame = callFrame.caller;
365         return callFrame;
366     },
367
368     _objectForId: function(objectId)
369     {
370         return this._idToWrappedObject[objectId.id];
371     },
372
373     nodeForObjectId: function(objectId)
374     {
375         var parsedObjectId = this._parseObjectId(objectId);
376         var object = this._objectForId(parsedObjectId);
377         if (!object || this._subtype(object) !== "node")
378             return null;
379         return object;
380     },
381
382     _isDefined: function(object)
383     {
384         return object || this._isHTMLAllCollection(object);
385     },
386
387     _isHTMLAllCollection: function(object)
388     {
389         // document.all is reported as undefined, but we still want to process it.
390         return (typeof object === "undefined") && InjectedScriptHost.isHTMLAllCollection(object);
391     },
392
393     _subtype: function(obj)
394     {
395         if (obj === null)
396             return "null";
397
398         var type = typeof obj;
399         if (this.isPrimitiveValue(obj))
400             return null;
401
402         if (this._isHTMLAllCollection(obj))
403             return "array";
404
405         var preciseType = InjectedScriptHost.type(obj);
406         if (preciseType)
407             return preciseType;
408
409         // FireBug's array detection.
410         try {
411             if (isFinite(obj.length) && typeof obj.splice === "function")
412                 return "array";
413             if (isFinite(obj.length) && typeof obj.callee === "function") // arguments.
414                 return "array";
415         } catch (e) {
416         }
417
418         // If owning frame has navigated to somewhere else window properties will be undefined.
419         return null;
420     },
421
422     _describe: function(obj)
423     {
424         if (this.isPrimitiveValue(obj))
425             return null;
426
427         // Type is object, get subtype.
428         var subtype = this._subtype(obj);
429
430         if (subtype === "regexp")
431             return this._toString(obj);
432
433         var className = InjectedScriptHost.internalConstructorName(obj);
434         if (subtype === "array") {
435             if (typeof obj.length === "number")
436                 className += "[" + obj.length + "]";
437             return className;
438         }
439
440         // NodeList in JSC is a function, check for array prior to this.
441         if (typeof obj === "function")
442             return this._toString(obj);
443
444         if (className === "Object") {
445             // In Chromium DOM wrapper prototypes will have Object as their constructor name,
446             // get the real DOM wrapper name from the constructor property.
447             var constructorName = obj.constructor && obj.constructor.name;
448             if (constructorName)
449                 return constructorName;
450         }
451         return className;
452     },
453
454     _toString: function(obj)
455     {
456         // We don't use String(obj) because inspectedWindow.String is undefined if owning frame navigated to another page.
457         return "" + obj;
458     }
459 }
460
461 var injectedScript = new InjectedScript();
462
463 InjectedScript.RemoteObject = function(object, objectGroupName, forceValueType)
464 {
465     this.type = typeof object;
466     if (injectedScript.isPrimitiveValue(object) || object === null || forceValueType) {
467         // We don't send undefined values over JSON.
468         if (typeof object !== "undefined")
469             this.value = object;
470
471         // Null object is object with 'null' subtype'
472         if (object === null)
473             this.subtype = "null";
474
475         // Provide user-friendly number values.
476         if (typeof object === "number")
477             this.description = object + "";
478         return;
479     }
480
481     this.objectId = injectedScript._bind(object, objectGroupName);
482     var subtype = injectedScript._subtype(object)
483     if (subtype)
484         this.subtype = subtype;
485     this.className = InjectedScriptHost.internalConstructorName(object);
486     this.description = injectedScript._describe(object);
487 }
488
489 InjectedScript.CallFrameProxy = function(ordinal, callFrame)
490 {
491     this.callFrameId = "{\"ordinal\":" + ordinal + ",\"injectedScriptId\":" + injectedScriptId + "}";
492     this.functionName = (callFrame.type === "function" ? callFrame.functionName : "");
493     this.location = { scriptId: String(callFrame.sourceID), lineNumber: callFrame.line, columnNumber: callFrame.column };
494     this.scopeChain = this._wrapScopeChain(callFrame);
495     this.this = injectedScript._wrapObject(callFrame.thisObject, "backtrace");
496 }
497
498 InjectedScript.CallFrameProxy.prototype = {
499     _wrapScopeChain: function(callFrame)
500     {
501         const GLOBAL_SCOPE = 0;
502         const LOCAL_SCOPE = 1;
503         const WITH_SCOPE = 2;
504         const CLOSURE_SCOPE = 3;
505         const CATCH_SCOPE = 4;
506
507         var scopeTypeNames = {};
508         scopeTypeNames[GLOBAL_SCOPE] = "global";
509         scopeTypeNames[LOCAL_SCOPE] = "local";
510         scopeTypeNames[WITH_SCOPE] = "with";
511         scopeTypeNames[CLOSURE_SCOPE] = "closure";
512         scopeTypeNames[CATCH_SCOPE] = "catch";
513
514         var scopeChain = callFrame.scopeChain;
515         var scopeChainProxy = [];
516         var foundLocalScope = false;
517         for (var i = 0; i < scopeChain.length; i++) {
518             var scope = {};
519             scope.object = injectedScript._wrapObject(scopeChain[i], "backtrace");
520
521             var scopeType = callFrame.scopeType(i);
522             scope.type = scopeTypeNames[scopeType];
523             scopeChainProxy.push(scope);
524         }
525         return scopeChainProxy;
526     }
527 }
528
529 function CommandLineAPI(commandLineAPIImpl, callFrame)
530 {
531     function inScopeVariables(member)
532     {
533         if (!callFrame)
534             return false;
535
536         var scopeChain = callFrame.scopeChain;
537         for (var i = 0; i < scopeChain.length; ++i) {
538             if (member in scopeChain[i])
539                 return true;
540         }
541         return false;
542     }
543
544     for (var i = 0; i < CommandLineAPI.members_.length; ++i) {
545         var member = CommandLineAPI.members_[i];
546         if (member in inspectedWindow || inScopeVariables(member))
547             continue;
548
549         this[member] = bind(commandLineAPIImpl, commandLineAPIImpl[member]);
550     }
551
552     for (var i = 0; i < 5; ++i) {
553         var member = "$" + i;
554         if (member in inspectedWindow || inScopeVariables(member))
555             continue;
556
557         this.__defineGetter__("$" + i, bind(commandLineAPIImpl, commandLineAPIImpl._inspectedNode, i));
558     }
559 }
560
561 CommandLineAPI.members_ = [
562     "$", "$$", "$x", "dir", "dirxml", "keys", "values", "profile", "profileEnd",
563     "monitorEvents", "unmonitorEvents", "inspect", "copy", "clear"
564 ];
565
566 function CommandLineAPIImpl()
567 {
568 }
569
570 CommandLineAPIImpl.prototype = {
571     $: function()
572     {
573         return document.getElementById.apply(document, arguments)
574     },
575
576     $$: function()
577     {
578         return document.querySelectorAll.apply(document, arguments)
579     },
580
581     $x: function(xpath, context)
582     {
583         var nodes = [];
584         try {
585             var doc = (context && context.ownerDocument) || inspectedWindow.document;
586             var results = doc.evaluate(xpath, context || doc, null, XPathResult.ANY_TYPE, null);
587             var node;
588             while (node = results.iterateNext())
589                 nodes.push(node);
590         } catch (e) {
591         }
592         return nodes;
593     },
594
595     dir: function()
596     {
597         return console.dir.apply(console, arguments)
598     },
599
600     dirxml: function()
601     {
602         return console.dirxml.apply(console, arguments)
603     },
604
605     keys: function(object)
606     {
607         return Object.keys(object);
608     },
609
610     values: function(object)
611     {
612         var result = [];
613         for (var key in object)
614             result.push(object[key]);
615         return result;
616     },
617
618     profile: function()
619     {
620         return console.profile.apply(console, arguments)
621     },
622
623     profileEnd: function()
624     {
625         return console.profileEnd.apply(console, arguments)
626     },
627
628     monitorEvents: function(object, types)
629     {
630         if (!object || !object.addEventListener || !object.removeEventListener)
631             return;
632         types = this._normalizeEventTypes(types);
633         for (var i = 0; i < types.length; ++i) {
634             object.removeEventListener(types[i], this._logEvent, false);
635             object.addEventListener(types[i], this._logEvent, false);
636         }
637     },
638
639     unmonitorEvents: function(object, types)
640     {
641         if (!object || !object.addEventListener || !object.removeEventListener)
642             return;
643         types = this._normalizeEventTypes(types);
644         for (var i = 0; i < types.length; ++i)
645             object.removeEventListener(types[i], this._logEvent, false);
646     },
647
648     inspect: function(object)
649     {
650         return injectedScript._inspect(object);
651     },
652
653     copy: function(object)
654     {
655         if (injectedScript._subtype(object) === "node")
656             object = object.outerHTML;
657         InjectedScriptHost.copyText(object);
658     },
659
660     clear: function()
661     {
662         InjectedScriptHost.clearConsoleMessages();
663     },
664
665     _inspectedNode: function(num)
666     {
667         return InjectedScriptHost.inspectedNode(num);
668     },
669
670     _normalizeEventTypes: function(types)
671     {
672         if (typeof types === "undefined")
673             types = [ "mouse", "key", "control", "load", "unload", "abort", "error", "select", "change", "submit", "reset", "focus", "blur", "resize", "scroll", "search", "devicemotion", "deviceorientation" ];
674         else if (typeof types === "string")
675             types = [ types ];
676
677         var result = [];
678         for (var i = 0; i < types.length; i++) {
679             if (types[i] === "mouse")
680                 result.splice(0, 0, "mousedown", "mouseup", "click", "dblclick", "mousemove", "mouseover", "mouseout", "mousewheel");
681             else if (types[i] === "key")
682                 result.splice(0, 0, "keydown", "keyup", "keypress", "textInput");
683             else if (types[i] === "control")
684                 result.splice(0, 0, "resize", "scroll", "zoom", "focus", "blur", "select", "change", "submit", "reset");
685             else
686                 result.push(types[i]);
687         }
688         return result;
689     },
690
691     _logEvent: function(event)
692     {
693         console.log(event.type, event);
694     }
695 }
696
697 injectedScript._commandLineAPIImpl = new CommandLineAPIImpl();
698 return injectedScript;
699 })