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