67ba846ed3add162823df61685e8f6fcb67a8f1e
[WebKit-https.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)
112     {
113         try {
114             return new InjectedScript.RemoteObject(object, objectGroupName);
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, ignoreHasOwnProperty)
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 properties = [];
177
178         var propertyNames = ignoreHasOwnProperty ? this._getPropertyNames(object) : Object.getOwnPropertyNames(object);
179         if (!ignoreHasOwnProperty && object.__proto__)
180             propertyNames.push("__proto__");
181
182         // Go over properties, prepare results.
183         for (var i = 0; i < propertyNames.length; ++i) {
184             var propertyName = propertyNames[i];
185
186             var getter = object["__lookupGetter__"] && object.__lookupGetter__(propertyName);
187             var setter = object["__lookupSetter__"] && object.__lookupSetter__(propertyName);
188             if (getter || setter) {
189                 if (getter) {
190                     var property = {};
191                     property.name = "get " + propertyName;
192                     property.value = this._wrapObject(getter, objectGroupName);
193                     properties.push(property);
194                 }
195                 if (setter) {
196                     var property = {};
197                     property.name = "set " + propertyName;
198                     property.value = this._wrapObject(setter, objectGroupName);
199                     properties.push(property);
200                 }
201             } else {
202                 var property = {};
203                 property.name = propertyName;
204                 var value;
205                 try {
206                     value = object[propertyName];
207                 } catch(e) {
208                     var value = e;
209                     property.wasThrown = true;
210                 }
211                 property.value = this._wrapObject(value, objectGroupName);
212                 properties.push(property);
213             }
214         }
215         return properties;
216     },
217
218     releaseObject: function(objectId)
219     {
220         var parsedObjectId = this._parseObjectId(objectId);
221         this._releaseObject(parsedObjectId.id);
222     },
223
224     _releaseObject: function(id)
225     {
226         delete this._idToWrappedObject[id];
227         delete this._idToObjectGroupName[id];
228     },
229
230     _populatePropertyNames: function(object, resultSet)
231     {
232         for (var o = object; o; o = o.__proto__) {
233             try {
234                 var names = Object.getOwnPropertyNames(o);
235                 for (var i = 0; i < names.length; ++i)
236                     resultSet[names[i]] = true;
237             } catch (e) {
238             }
239         }
240     },
241
242     _getPropertyNames: function(object, resultSet)
243     {
244         var propertyNameSet = {};
245         this._populatePropertyNames(object, propertyNameSet);
246         return Object.keys(propertyNameSet);
247     },
248
249     evaluate: function(expression, objectGroup, injectCommandLineAPI)
250     {
251         return this._evaluateAndWrap(InjectedScriptHost.evaluate, InjectedScriptHost, expression, objectGroup, false, injectCommandLineAPI);
252     },
253
254     callFunctionOn: function(objectId, expression, args)
255     {
256         var parsedObjectId = this._parseObjectId(objectId);
257         var object = this._objectForId(parsedObjectId);
258         if (!object)
259             return "Could not find object with given id";
260
261         if (args) {
262             var resolvedArgs = [];
263             args = InjectedScriptHost.evaluate(args);
264             for (var i = 0; i < args.length; ++i) {
265                 var objectId = args[i].objectId;
266                 if (objectId) {
267                     var parsedArgId = this._parseObjectId(objectId);
268                     if (!parsedArgId || parsedArgId.injectedScriptId !== injectedScriptId)
269                         return "Arguments should belong to the same JavaScript world as the target object.";
270
271                     var resolvedArg = this._objectForId(parsedArgId);
272                     if (!resolvedArg)
273                         return "Could not find object with given id";
274
275                     resolvedArgs.push(resolvedArg);
276                 } else if (args[i].value)
277                     resolvedArgs.push(args[i].value);
278                 else
279                     resolvedArgs.push(undefined);
280             }
281         }
282
283         try {
284             var objectGroup = this._idToObjectGroupName[parsedObjectId.id];
285             var func = InjectedScriptHost.evaluate("(" + expression + ")");
286             if (typeof func !== "function")
287                 return "Given expression does not evaluate to a function";
288
289             return { wasThrown: false,
290                      result: this._wrapObject(func.apply(object, resolvedArgs), objectGroup) };
291         } catch (e) {
292             return { wasThrown: true,
293                      result: this._wrapObject(e, objectGroup) };
294         }
295     },
296
297     _evaluateAndWrap: function(evalFunction, object, expression, objectGroup, isEvalOnCallFrame, injectCommandLineAPI)
298     {
299         try {
300             return { wasThrown: false,
301                      result: this._wrapObject(this._evaluateOn(evalFunction, object, expression, isEvalOnCallFrame, injectCommandLineAPI), objectGroup) };
302         } catch (e) {
303             return { wasThrown: true,
304                      result: this._wrapObject(e, objectGroup) };
305         }
306     },
307
308     _evaluateOn: function(evalFunction, object, expression, isEvalOnCallFrame, injectCommandLineAPI)
309     {
310         // Only install command line api object for the time of evaluation.
311         // Surround the expression in with statements to inject our command line API so that
312         // the window object properties still take more precedent than our API functions.
313
314         try {
315             if (injectCommandLineAPI && inspectedWindow.console) {
316                 inspectedWindow.console._commandLineAPI = new CommandLineAPI(this._commandLineAPIImpl, isEvalOnCallFrame ? object : null);
317                 expression = "with ((window && window.console && window.console._commandLineAPI) || {}) {\n" + expression + "\n}";
318             }
319             return evalFunction.call(object, expression);
320         } finally {
321             if (injectCommandLineAPI && inspectedWindow.console)
322                 delete inspectedWindow.console._commandLineAPI;
323         }
324     },
325
326     wrapCallFrames: function(callFrame)
327     {
328         if (!callFrame)
329             return false;
330
331         var result = [];
332         var depth = 0;
333         do {
334             result.push(new InjectedScript.CallFrameProxy(depth++, callFrame));
335             callFrame = callFrame.caller;
336         } while (callFrame);
337         return result;
338     },
339
340     evaluateOnCallFrame: function(topCallFrame, callFrameId, expression, objectGroup, injectCommandLineAPI)
341     {
342         var callFrame = this._callFrameForId(topCallFrame, callFrameId);
343         if (!callFrame)
344             return "Could not find call frame with given id";
345         return this._evaluateAndWrap(callFrame.evaluate, callFrame, expression, objectGroup, true, injectCommandLineAPI);
346     },
347
348     _callFrameForId: function(topCallFrame, callFrameId)
349     {
350         var parsedCallFrameId = InjectedScriptHost.evaluate("(" + callFrameId + ")");
351         var ordinal = parsedCallFrameId.ordinal;
352         var callFrame = topCallFrame;
353         while (--ordinal >= 0 && callFrame)
354             callFrame = callFrame.caller;
355         return callFrame;
356     },
357
358     _objectForId: function(objectId)
359     {
360         return this._idToWrappedObject[objectId.id];
361     },
362
363     nodeForObjectId: function(objectId)
364     {
365         var parsedObjectId = this._parseObjectId(objectId);
366         var object = this._objectForId(parsedObjectId);
367         if (!object || this._subtype(object) !== "node")
368             return null;
369         return object;
370     },
371
372     _isDefined: function(object)
373     {
374         return object || this._isHTMLAllCollection(object);
375     },
376
377     _isHTMLAllCollection: function(object)
378     {
379         // document.all is reported as undefined, but we still want to process it.
380         return (typeof object === "undefined") && InjectedScriptHost.isHTMLAllCollection(object);
381     },
382
383     _subtype: function(obj)
384     {
385         if (obj === null)
386             return "null";
387
388         var type = typeof obj;
389         if (this.isPrimitiveValue(obj))
390             return null;
391
392         if (this._isHTMLAllCollection(obj))
393             return "array";
394
395         var preciseType = InjectedScriptHost.type(obj);
396         if (preciseType)
397             return preciseType;
398
399         // FireBug's array detection.
400         try {
401             if (isFinite(obj.length) && typeof obj.splice === "function")
402                 return "array";
403             if (isFinite(obj.length) && typeof obj.callee === "function") // arguments.
404                 return "array";
405         } catch (e) {
406         }
407
408         // If owning frame has navigated to somewhere else window properties will be undefined.
409         return null;
410     },
411
412     _describe: function(obj)
413     {
414         if (this.isPrimitiveValue(obj))
415             return null;
416
417         // Type is object, get subtype.
418         var subtype = this._subtype(obj);
419
420         if (subtype === "regexp")
421             return this._toString(obj);
422
423         var className = InjectedScriptHost.internalConstructorName(obj);
424         if (subtype === "array") {
425             if (typeof obj.length === "number")
426                 className += "[" + obj.length + "]";
427             return className;
428         }
429
430         // NodeList in JSC is a function, check for array prior to this.
431         if (typeof obj === "function")
432             return this._toString(obj);
433
434         if (className === "Object") {
435             // In Chromium DOM wrapper prototypes will have Object as their constructor name,
436             // get the real DOM wrapper name from the constructor property.
437             var constructorName = obj.constructor && obj.constructor.name;
438             if (constructorName)
439                 return constructorName;
440         }
441         return className;
442     },
443
444     _toString: function(obj)
445     {
446         // We don't use String(obj) because inspectedWindow.String is undefined if owning frame navigated to another page.
447         return "" + obj;
448     }
449 }
450
451 var injectedScript = new InjectedScript();
452
453 InjectedScript.RemoteObject = function(object, objectGroupName)
454 {
455     this.type = typeof object;
456     if (injectedScript.isPrimitiveValue(object) || object === null) {
457
458         // We don't send undefined values over JSON.
459         if (typeof object !== "undefined")
460             this.value = object;
461
462         // Null object is object with 'null' subtype'
463         if (object === null)
464             this.subtype = "null";
465
466         // Provide user-friendly number values.
467         if (typeof object === "number")
468             this.description = object + "";
469         return;
470     }
471
472     this.objectId = injectedScript._bind(object, objectGroupName);
473     var subtype = injectedScript._subtype(object)
474     if (subtype)
475         this.subtype = subtype;
476     this.className = InjectedScriptHost.internalConstructorName(object);
477     this.description = injectedScript._describe(object);
478 }
479
480 InjectedScript.CallFrameProxy = function(ordinal, callFrame)
481 {
482     this.id = "{\"ordinal\":" + ordinal + ",\"injectedScriptId\":" + injectedScriptId + "}";
483     this.functionName = (callFrame.type === "function" ? callFrame.functionName : "");
484     this.location = { sourceId: String(callFrame.sourceID), lineNumber: callFrame.line, columnNumber: callFrame.column };
485     this.scopeChain = this._wrapScopeChain(callFrame);
486     this.this = injectedScript._wrapObject(callFrame.thisObject, "backtrace");
487 }
488
489 InjectedScript.CallFrameProxy.prototype = {
490     _wrapScopeChain: function(callFrame)
491     {
492         const GLOBAL_SCOPE = 0;
493         const LOCAL_SCOPE = 1;
494         const WITH_SCOPE = 2;
495         const CLOSURE_SCOPE = 3;
496         const CATCH_SCOPE = 4;
497
498         var scopeTypeNames = {};
499         scopeTypeNames[GLOBAL_SCOPE] = "global";
500         scopeTypeNames[LOCAL_SCOPE] = "local";
501         scopeTypeNames[WITH_SCOPE] = "with";
502         scopeTypeNames[CLOSURE_SCOPE] = "closure";
503         scopeTypeNames[CATCH_SCOPE] = "catch";
504
505         var scopeChain = callFrame.scopeChain;
506         var scopeChainProxy = [];
507         var foundLocalScope = false;
508         for (var i = 0; i < scopeChain.length; i++) {
509             var scope = {};
510             scope.object = injectedScript._wrapObject(scopeChain[i], "backtrace");
511
512             var scopeType = callFrame.scopeType(i);
513             scope.type = scopeTypeNames[scopeType];
514             scopeChainProxy.push(scope);
515         }
516         return scopeChainProxy;
517     }
518 }
519
520 function CommandLineAPI(commandLineAPIImpl, callFrame)
521 {
522     function inScopeVariables(member)
523     {
524         if (!callFrame)
525             return false;
526
527         var scopeChain = callFrame.scopeChain;
528         for (var i = 0; i < scopeChain.length; ++i) {
529             if (member in scopeChain[i])
530                 return true;
531         }
532         return false;
533     }
534
535     for (var i = 0; i < CommandLineAPI.members_.length; ++i) {
536         var member = CommandLineAPI.members_[i];
537         if (member in inspectedWindow || inScopeVariables(member))
538             continue;
539
540         this[member] = bind(commandLineAPIImpl, commandLineAPIImpl[member]);
541     }
542
543     for (var i = 0; i < 5; ++i) {
544         var member = "$" + i;
545         if (member in inspectedWindow || inScopeVariables(member))
546             continue;
547
548         this.__defineGetter__("$" + i, bind(commandLineAPIImpl, commandLineAPIImpl._inspectedNode, i));
549     }
550 }
551
552 CommandLineAPI.members_ = [
553     "$", "$$", "$x", "dir", "dirxml", "keys", "values", "profile", "profileEnd",
554     "monitorEvents", "unmonitorEvents", "inspect", "copy", "clear"
555 ];
556
557 function CommandLineAPIImpl()
558 {
559 }
560
561 CommandLineAPIImpl.prototype = {
562     $: function()
563     {
564         return document.getElementById.apply(document, arguments)
565     },
566
567     $$: function()
568     {
569         return document.querySelectorAll.apply(document, arguments)
570     },
571
572     $x: function(xpath, context)
573     {
574         var nodes = [];
575         try {
576             var doc = (context && context.ownerDocument) || inspectedWindow.document;
577             var results = doc.evaluate(xpath, context || doc, null, XPathResult.ANY_TYPE, null);
578             var node;
579             while (node = results.iterateNext())
580                 nodes.push(node);
581         } catch (e) {
582         }
583         return nodes;
584     },
585
586     dir: function()
587     {
588         return console.dir.apply(console, arguments)
589     },
590
591     dirxml: function()
592     {
593         return console.dirxml.apply(console, arguments)
594     },
595
596     keys: function(object)
597     {
598         return Object.keys(object);
599     },
600
601     values: function(object)
602     {
603         var result = [];
604         for (var key in object)
605             result.push(object[key]);
606         return result;
607     },
608
609     profile: function()
610     {
611         return console.profile.apply(console, arguments)
612     },
613
614     profileEnd: function()
615     {
616         return console.profileEnd.apply(console, arguments)
617     },
618
619     monitorEvents: function(object, types)
620     {
621         if (!object || !object.addEventListener || !object.removeEventListener)
622             return;
623         types = this._normalizeEventTypes(types);
624         for (var i = 0; i < types.length; ++i) {
625             object.removeEventListener(types[i], this._logEvent, false);
626             object.addEventListener(types[i], this._logEvent, false);
627         }
628     },
629
630     unmonitorEvents: function(object, types)
631     {
632         if (!object || !object.addEventListener || !object.removeEventListener)
633             return;
634         types = this._normalizeEventTypes(types);
635         for (var i = 0; i < types.length; ++i)
636             object.removeEventListener(types[i], this._logEvent, false);
637     },
638
639     inspect: function(object)
640     {
641         return injectedScript._inspect(object);
642     },
643
644     copy: function(object)
645     {
646         if (injectedScript._subtype(object) === "node")
647             object = object.outerHTML;
648         InjectedScriptHost.copyText(object);
649     },
650
651     clear: function()
652     {
653         InjectedScriptHost.clearConsoleMessages();
654     },
655
656     _inspectedNode: function(num)
657     {
658         return InjectedScriptHost.inspectedNode(num);
659     },
660
661     _normalizeEventTypes: function(types)
662     {
663         if (typeof types === "undefined")
664             types = [ "mouse", "key", "load", "unload", "abort", "error", "select", "change", "submit", "reset", "focus", "blur", "resize", "scroll" ];
665         else if (typeof types === "string")
666             types = [ types ];
667
668         var result = [];
669         for (var i = 0; i < types.length; i++) {
670             if (types[i] === "mouse")
671                 result.splice(0, 0, "mousedown", "mouseup", "click", "dblclick", "mousemove", "mouseover", "mouseout");
672             else if (types[i] === "key")
673                 result.splice(0, 0, "keydown", "keyup", "keypress");
674             else
675                 result.push(types[i]);
676         }
677         return result;
678     },
679
680     _logEvent: function(event)
681     {
682         console.log(event.type, event);
683     }
684 }
685
686 injectedScript._commandLineAPIImpl = new CommandLineAPIImpl();
687 return injectedScript;
688 })