c11475033b0f4b5f17c76dea02b48dce0235f719
[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.prototype = {
54     wrapObject: function(object, groupName, canAccessInspectedWindow)
55     {
56         if (canAccessInspectedWindow)
57             return this._wrapObject(object, groupName);
58         var result = {};
59         result.type = typeof object;
60         result.description = this._toString(object);
61         return result;
62     },
63
64     inspectNode: function(object)
65     {
66         this._inspect(object);
67     },
68
69     _inspect: function(object)
70     {
71         if (arguments.length === 0)
72             return;
73
74         var objectId = this._wrapObject(object, "", false);
75         var hints = {};
76
77         switch (injectedScript._describe(object)) {
78             case "Database":
79                 var databaseId = InjectedScriptHost.databaseId(object)
80                 if (databaseId)
81                     hints.databaseId = databaseId;
82                 break;
83             case "Storage":
84                 var storageId = InjectedScriptHost.storageId(object)
85                 if (storageId)
86                     hints.domStorageId = storageId;
87                 break;
88         }
89         InjectedScriptHost.inspect(objectId, hints);
90         return object;
91     },
92
93     _wrapObject: function(object, objectGroupName, abbreviate)
94     {
95         try {
96             if (typeof object === "object" || typeof object === "function" || this._isHTMLAllCollection(object)) {
97                 var id = this._lastBoundObjectId++;
98                 this._idToWrappedObject[id] = object;
99                 var objectId = { injectedScriptId: injectedScriptId, id: id };
100     
101                 if (objectGroupName) {
102                     var group = this._objectGroups[objectGroupName];
103                     if (!group) {
104                         group = [];
105                         this._objectGroups[objectGroupName] = group;
106                     }
107                     group.push(id);
108                     this._idToObjectGroupName[id] = objectGroupName;
109                 }
110             }
111             return InjectedScript.RemoteObject.fromObject(object, objectId, abbreviate);
112         } catch (e) {
113             return InjectedScript.RemoteObject.fromObject("[ Exception: " + e.toString() + " ]");
114         }
115     },
116
117     _parseObjectId: function(objectId)
118     {
119         return eval("(" + objectId + ")");
120     },
121
122     releaseObjectGroup: function(objectGroupName)
123     {
124         var group = this._objectGroups[objectGroupName];
125         if (!group)
126             return;
127         for (var i = 0; i < group.length; i++)
128             this._releaseObject(group[i]);
129         delete this._objectGroups[objectGroupName];
130     },
131
132     dispatch: function(methodName, args)
133     {
134         var argsArray = eval("(" + args + ")");
135         var result = this[methodName].apply(this, argsArray);
136         if (typeof result === "undefined") {
137             inspectedWindow.console.error("Web Inspector error: InjectedScript.%s returns undefined", methodName);
138             result = null;
139         }
140         return result;
141     },
142
143     getProperties: function(objectId, ignoreHasOwnProperty, abbreviate)
144     {
145         var parsedObjectId = this._parseObjectId(objectId);
146         var object = this._objectForId(parsedObjectId);
147         var objectGroupName = this._idToObjectGroupName[parsedObjectId.id];
148
149         if (!this._isDefined(object))
150             return false;
151         var properties = [];
152
153         var propertyNames = ignoreHasOwnProperty ? this._getPropertyNames(object) : Object.getOwnPropertyNames(object);
154         if (!ignoreHasOwnProperty && object.__proto__)
155             propertyNames.push("__proto__");
156     
157         // Go over properties, prepare results.
158         for (var i = 0; i < propertyNames.length; ++i) {
159             var propertyName = propertyNames[i];
160     
161             var property = {};
162             property.name = propertyName + "";
163             var isGetter = object["__lookupGetter__"] && object.__lookupGetter__(propertyName);
164             if (!isGetter) {
165                 try {
166                     property.value = this._wrapObject(object[propertyName], objectGroupName, abbreviate);
167                 } catch(e) {
168                     property.value = new InjectedScript.RemoteObject.fromException(e);
169                 }
170             } else {
171                 // FIXME: this should show something like "getter" (bug 16734).
172                 property.value = new InjectedScript.RemoteObject.fromObject("\u2014"); // em dash
173                 property.isGetter = true;
174             }
175             properties.push(property);
176         }
177         return properties;
178     },
179
180     setPropertyValue: function(objectId, propertyName, expression)
181     {
182         var parsedObjectId = this._parseObjectId(objectId);
183         var object = this._objectForId(parsedObjectId);
184         if (!this._isDefined(object))
185             return false;
186     
187         var expressionLength = expression.length;
188         if (!expressionLength) {
189             delete object[propertyName];
190             return !(propertyName in object);
191         }
192     
193         try {
194             // Surround the expression in parenthesis so the result of the eval is the result
195             // of the whole expression not the last potential sub-expression.
196     
197             // There is a regression introduced here: eval is now happening against global object,
198             // not call frame while on a breakpoint.
199             // TODO: bring evaluation against call frame back.
200             var result = inspectedWindow.eval("(" + expression + ")");
201             // Store the result in the property.
202             object[propertyName] = result;
203             return true;
204         } catch(e) {
205             try {
206                 var result = inspectedWindow.eval("\"" + expression.replace(/"/g, "\\\"") + "\"");
207                 object[propertyName] = result;
208                 return true;
209             } catch(e) {
210                 return false;
211             }
212         }
213     },
214
215     releaseObject: function(objectId)
216     {
217         var parsedObjectId = this._parseObjectId(objectId);
218         this._releaseObject(parsedObjectId.id);
219     },
220
221     _releaseObject: function(id)
222     {
223         delete this._idToWrappedObject[id];
224         delete this._idToObjectGroupName[id];
225     },
226
227     _populatePropertyNames: function(object, resultSet)
228     {
229         for (var o = object; o; o = o.__proto__) {
230             try {
231                 var names = Object.getOwnPropertyNames(o);
232                 for (var i = 0; i < names.length; ++i)
233                     resultSet[names[i]] = true;
234             } catch (e) {
235             }
236         }
237     },
238
239     _getPropertyNames: function(object, resultSet)
240     {
241         var propertyNameSet = {};
242         this._populatePropertyNames(object, propertyNameSet);
243         return Object.keys(propertyNameSet);
244     },
245
246     evaluate: function(expression, objectGroup, injectCommandLineAPI)
247     {
248         return this._evaluateAndWrap(inspectedWindow.eval, inspectedWindow, expression, objectGroup, false, injectCommandLineAPI);
249     },
250
251     evaluateOn: function(objectId, expression)
252     {
253         var parsedObjectId = this._parseObjectId(objectId);
254         var object = this._objectForId(parsedObjectId);
255         if (!object)
256             return false;
257         try {
258             inspectedWindow.console._objectToEvaluateOn = object;
259             return this._evaluateAndWrap(inspectedWindow.eval, inspectedWindow, "(function() {" + expression + "}).call(window.console._objectToEvaluateOn)", parsedObjectId.objectGroup, false, false);
260         } finally {
261             delete inspectedWindow.console._objectToEvaluateOn;
262         }
263     },
264
265     _evaluateAndWrap: function(evalFunction, object, expression, objectGroup, isEvalOnCallFrame, injectCommandLineAPI)
266     {
267         try {
268             return this._wrapObject(this._evaluateOn(evalFunction, object, expression, isEvalOnCallFrame, injectCommandLineAPI), objectGroup);
269         } catch (e) {
270             return InjectedScript.RemoteObject.fromException(e);
271         }
272     },
273
274     _evaluateOn: function(evalFunction, object, expression, isEvalOnCallFrame, injectCommandLineAPI)
275     {
276         // Only install command line api object for the time of evaluation.
277         // Surround the expression in with statements to inject our command line API so that
278         // the window object properties still take more precedent than our API functions.
279
280         try {
281             if (injectCommandLineAPI && inspectedWindow.console) {
282                 inspectedWindow.console._commandLineAPI = new CommandLineAPI(this._commandLineAPIImpl, isEvalOnCallFrame ? object : null);
283                 expression = "with ((window && window.console && window.console._commandLineAPI) || {}) {\n" + expression + "\n}";
284             }
285
286             var value = evalFunction.call(object, expression);
287
288             // When evaluating on call frame error is not thrown, but returned as a value.
289             if (this._type(value) === "error")
290                 throw value.toString();
291
292             return value;
293         } finally {
294             if (injectCommandLineAPI && inspectedWindow.console)
295                 delete inspectedWindow.console._commandLineAPI;
296         }
297     },
298
299     callFrames: function()
300     {
301         var callFrame = InjectedScriptHost.currentCallFrame();
302         if (!callFrame)
303             return false;
304     
305         injectedScript.releaseObjectGroup("backtrace");
306         var result = [];
307         var depth = 0;
308         do {
309             result.push(new InjectedScript.CallFrameProxy(depth++, callFrame));
310             callFrame = callFrame.caller;
311         } while (callFrame);
312         return result;
313     },
314
315     evaluateOnCallFrame: function(callFrameId, expression, objectGroup, injectCommandLineAPI)
316     {
317         var callFrame = this._callFrameForId(callFrameId);
318         if (!callFrame)
319             return false;
320         return this._evaluateAndWrap(callFrame.evaluate, callFrame, expression, objectGroup, true, injectCommandLineAPI);
321     },
322
323     _callFrameForId: function(callFrameId)
324     {
325         var parsedCallFrameId = eval("(" + callFrameId + ")");
326         var ordinal = parsedCallFrameId.ordinal;
327         var callFrame = InjectedScriptHost.currentCallFrame();
328         while (--ordinal >= 0 && callFrame)
329             callFrame = callFrame.caller;
330         return callFrame;
331     },
332
333     _objectForId: function(objectId)
334     {
335         return this._idToWrappedObject[objectId.id];
336     },
337
338     nodeForObjectId: function(objectId)
339     {
340         var parsedObjectId = this._parseObjectId(objectId);
341         var object = this._objectForId(parsedObjectId);
342         if (!object || this._type(object) !== "node")
343             return null;
344         return object;
345     },
346
347     resolveNode: function(node)
348     {
349         return this._wrapObject(node);
350     },
351
352     _isDefined: function(object)
353     {
354         return object || this._isHTMLAllCollection(object);
355     },
356
357     _isHTMLAllCollection: function(object)
358     {
359         // document.all is reported as undefined, but we still want to process it.
360         return (typeof object === "undefined") && inspectedWindow.HTMLAllCollection && object instanceof inspectedWindow.HTMLAllCollection;
361     },
362
363     _type: function(obj)
364     {
365         if (obj === null)
366             return "null";
367
368         var type = typeof obj;
369         if (type !== "object" && type !== "function") {
370             // FIXME(33716): typeof document.all is always 'undefined'.
371             if (this._isHTMLAllCollection(obj))
372                 return "array";
373             return type;
374         }
375
376         // If owning frame has navigated to somewhere else window properties will be undefined.
377         // In this case just return result of the typeof.
378         if (!inspectedWindow.document)
379             return type;
380
381         if (obj instanceof inspectedWindow.Node)
382             return (obj.nodeType === undefined ? type : "node");
383         if (obj instanceof inspectedWindow.String)
384             return "string";
385         if (obj instanceof inspectedWindow.Array)
386             return "array";
387         if (obj instanceof inspectedWindow.Boolean)
388             return "boolean";
389         if (obj instanceof inspectedWindow.Number)
390             return "number";
391         if (obj instanceof inspectedWindow.Date)
392             return "date";
393         if (obj instanceof inspectedWindow.RegExp)
394             return "regexp";
395         // FireBug's array detection.
396         if (isFinite(obj.length) && typeof obj.splice === "function")
397             return "array";
398         if (isFinite(obj.length) && typeof obj.callee === "function") // arguments.
399             return "array";
400         if (obj instanceof inspectedWindow.NodeList)
401             return "array";
402         if (obj instanceof inspectedWindow.HTMLCollection)
403             return "array";
404         if (obj instanceof inspectedWindow.Error)
405             return "error";
406         return type;
407     },
408
409     _describe: function(obj, abbreviated)
410     {
411         var type = this._type(obj);
412
413         switch (type) {
414         case "object":
415         case "node":
416             var result = InjectedScriptHost.internalConstructorName(obj);
417             if (result === "Object") {
418                 // In Chromium DOM wrapper prototypes will have Object as their constructor name,
419                 // get the real DOM wrapper name from the constructor property.
420                 var constructorName = obj.constructor && obj.constructor.name;
421                 if (constructorName)
422                     return constructorName;
423             }
424             return result;
425         case "array":
426             var className = InjectedScriptHost.internalConstructorName(obj);
427             if (typeof obj.length === "number")
428                 className += "[" + obj.length + "]";
429             return className;
430         case "string":
431             if (!abbreviated)
432                 return obj;
433             if (obj.length > 100)
434                 return "\"" + obj.substring(0, 100) + "\u2026\"";
435             return "\"" + obj + "\"";
436         case "function":
437             var objectText = this._toString(obj);
438             if (abbreviated)
439                 objectText = /.*/.exec(objectText)[0].replace(/ +$/g, "");
440             return objectText;
441         default:
442             return this._toString(obj);
443         }
444     },
445
446     _toString: function(obj)
447     {
448         // We don't use String(obj) because inspectedWindow.String is undefined if owning frame navigated to another page.
449         return "" + obj;
450     }
451 }
452
453 var injectedScript = new InjectedScript();
454
455 InjectedScript.RemoteObject = function(objectId, type, description, hasChildren)
456 {
457     this.objectId = objectId;
458     this.type = type;
459     this.description = description;
460     this.hasChildren = hasChildren;
461 }
462
463 InjectedScript.RemoteObject.fromException = function(e)
464 {
465     return new InjectedScript.RemoteObject(null, "error", e.toString());
466 }
467
468 InjectedScript.RemoteObject.fromObject = function(object, objectId, abbreviate)
469 {
470     var type = injectedScript._type(object);
471     var rawType = typeof object;
472     var hasChildren = (rawType === "object" && object !== null && (!!Object.getOwnPropertyNames(object).length || !!object.__proto__)) || rawType === "function";
473     var description = "";
474     try {
475         var description = injectedScript._describe(object, abbreviate);
476         return new InjectedScript.RemoteObject(objectId, type, description, hasChildren);
477     } catch (e) {
478         return InjectedScript.RemoteObject.fromException(e);
479     }
480 }
481
482 InjectedScript.CallFrameProxy = function(ordinal, callFrame)
483 {
484     this.id = { ordinal: ordinal, injectedScriptId: injectedScriptId };
485     this.type = callFrame.type;
486     this.functionName = (this.type === "function" ? callFrame.functionName : "");
487     this.sourceID = callFrame.sourceID;
488     this.line = callFrame.line;
489     this.column = callFrame.column;
490     this.scopeChain = this._wrapScopeChain(callFrame);
491 }
492
493 InjectedScript.CallFrameProxy.prototype = {
494     _wrapScopeChain: function(callFrame)
495     {
496         const GLOBAL_SCOPE = 0;
497         const LOCAL_SCOPE = 1;
498         const WITH_SCOPE = 2;
499         const CLOSURE_SCOPE = 3;
500         const CATCH_SCOPE = 4;
501     
502         var scopeChain = callFrame.scopeChain;
503         var scopeChainProxy = [];
504         var foundLocalScope = false;
505         for (var i = 0; i < scopeChain.length; i++) {
506             var scopeType = callFrame.scopeType(i);
507             var scopeObject = scopeChain[i];
508             var scopeObjectProxy = injectedScript._wrapObject(scopeObject, "backtrace", true);
509
510             switch(scopeType) {
511                 case LOCAL_SCOPE: {
512                     foundLocalScope = true;
513                     scopeObjectProxy.isLocal = true;
514                     scopeObjectProxy.thisObject = injectedScript._wrapObject(callFrame.thisObject, "backtrace", true);
515                     break;
516                 }
517                 case CLOSURE_SCOPE: {
518                     scopeObjectProxy.isClosure = true;
519                     break;
520                 }
521                 case WITH_SCOPE:
522                 case CATCH_SCOPE: {
523                     if (foundLocalScope && scopeObject instanceof inspectedWindow.Element)
524                         scopeObjectProxy.isElement = true;
525                     else if (foundLocalScope && scopeObject instanceof inspectedWindow.Document)
526                         scopeObjectProxy.isDocument = true;
527                     else
528                         scopeObjectProxy.isWithBlock = true;
529                     break;
530                 }
531             }
532             scopeChainProxy.push(scopeObjectProxy);
533         }
534         return scopeChainProxy;
535     }
536 }
537
538 function CommandLineAPI(commandLineAPIImpl, callFrame)
539 {
540     function inScopeVariables(member)
541     {
542         if (!callFrame)
543             return false;
544
545         var scopeChain = callFrame.scopeChain;
546         for (var i = 0; i < scopeChain.length; ++i) {
547             if (member in scopeChain[i])
548                 return true;
549         }
550         return false;
551     }
552
553     for (var i = 0; i < CommandLineAPI.members_.length; ++i) {
554         var member = CommandLineAPI.members_[i];
555         if (member in inspectedWindow || inScopeVariables(member))
556             continue;
557
558         this[member] = bind(commandLineAPIImpl, commandLineAPIImpl[member]);
559     }
560
561     for (var i = 0; i < 5; ++i) {
562         var member = "$" + i;
563         if (member in inspectedWindow || inScopeVariables(member))
564             continue;
565
566         this.__defineGetter__("$" + i, bind(commandLineAPIImpl, commandLineAPIImpl._inspectedNode, i));
567     }
568 }
569
570 CommandLineAPI.members_ = [
571     "$", "$$", "$x", "dir", "dirxml", "keys", "values", "profile", "profileEnd",
572     "monitorEvents", "unmonitorEvents", "inspect", "copy", "clear"
573 ];
574
575 function CommandLineAPIImpl()
576 {
577 }
578
579 CommandLineAPIImpl.prototype = {
580     $: function()
581     {
582         return document.getElementById.apply(document, arguments)
583     },
584
585     $$: function()
586     {
587         return document.querySelectorAll.apply(document, arguments)
588     },
589
590     $x: function(xpath, context)
591     {
592         var nodes = [];
593         try {
594             var doc = (context && context.ownerDocument) || inspectedWindow.document;
595             var results = doc.evaluate(xpath, context || doc, null, XPathResult.ANY_TYPE, null);
596             var node;
597             while (node = results.iterateNext())
598                 nodes.push(node);
599         } catch (e) {
600         }
601         return nodes;
602     },
603
604     dir: function()
605     {
606         return console.dir.apply(console, arguments)
607     },
608
609     dirxml: function()
610     {
611         return console.dirxml.apply(console, arguments)
612     },
613
614     keys: function(object)
615     {
616         return Object.keys(object);
617     },
618
619     values: function(object)
620     {
621         var result = [];
622         for (var key in object)
623             result.push(object[key]);
624         return result;
625     },
626
627     profile: function()
628     {
629         return console.profile.apply(console, arguments)
630     },
631
632     profileEnd: function()
633     {
634         return console.profileEnd.apply(console, arguments)
635     },
636
637     monitorEvents: function(object, types)
638     {
639         if (!object || !object.addEventListener || !object.removeEventListener)
640             return;
641         types = this._normalizeEventTypes(types);
642         for (var i = 0; i < types.length; ++i) {
643             object.removeEventListener(types[i], this._logEvent, false);
644             object.addEventListener(types[i], this._logEvent, false);
645         }
646     },
647
648     unmonitorEvents: function(object, types)
649     {
650         if (!object || !object.addEventListener || !object.removeEventListener)
651             return;
652         types = this._normalizeEventTypes(types);
653         for (var i = 0; i < types.length; ++i)
654             object.removeEventListener(types[i], this._logEvent, false);
655     },
656
657     inspect: function(object)
658     {
659         return injectedScript._inspect(object);
660     },
661
662     copy: function(object)
663     {
664         if (injectedScript._type(object) === "node")
665             object = object.outerHTML;
666         InjectedScriptHost.copyText(object);
667     },
668
669     clear: function()
670     {
671         InjectedScriptHost.clearConsoleMessages();
672     },
673
674     _inspectedNode: function(num)
675     {
676         return InjectedScriptHost.inspectedNode(num);
677     },
678
679     _normalizeEventTypes: function(types)
680     {
681         if (typeof types === "undefined")
682             types = [ "mouse", "key", "load", "unload", "abort", "error", "select", "change", "submit", "reset", "focus", "blur", "resize", "scroll" ];
683         else if (typeof types === "string")
684             types = [ types ];
685
686         var result = [];
687         for (var i = 0; i < types.length; i++) {
688             if (types[i] === "mouse")
689                 result.splice(0, 0, "mousedown", "mouseup", "click", "dblclick", "mousemove", "mouseover", "mouseout");
690             else if (types[i] === "key")
691                 result.splice(0, 0, "keydown", "keyup", "keypress");
692             else
693                 result.push(types[i]);
694         }
695         return result;
696     },
697
698     _logEvent: function(event)
699     {
700         console.log(event.type, event);
701     }
702 }
703
704 injectedScript._commandLineAPIImpl = new CommandLineAPIImpl();
705 return injectedScript;
706 })