1/*
2 * Copyright (C) 2012 Google 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 are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31function defineCommonExtensionSymbols(apiPrivate)
32{
33    if (!apiPrivate.audits)
34        apiPrivate.audits = {};
35    apiPrivate.audits.Severity = {
36        Info: "info",
37        Warning: "warning",
38        Severe: "severe"
39    };
40
41    if (!apiPrivate.console)
42        apiPrivate.console = {};
43    apiPrivate.console.Severity = {
44        Debug: "debug",
45        Log: "log",
46        Warning: "warning",
47        Error: "error"
48    };
49
50    if (!apiPrivate.panels)
51        apiPrivate.panels = {};
52    apiPrivate.panels.SearchAction = {
53        CancelSearch: "cancelSearch",
54        PerformSearch: "performSearch",
55        NextSearchResult: "nextSearchResult",
56        PreviousSearchResult: "previousSearchResult"
57    };
58
59    apiPrivate.Events = {
60        AuditStarted: "audit-started-",
61        ButtonClicked: "button-clicked-",
62        ConsoleMessageAdded: "console-message-added",
63        PanelObjectSelected: "panel-objectSelected-",
64        NetworkRequestFinished: "network-request-finished",
65        OpenResource: "open-resource",
66        PanelSearch: "panel-search-",
67        ResourceAdded: "resource-added",
68        ResourceContentCommitted: "resource-content-committed",
69        ViewShown: "view-shown-",
70        ViewHidden: "view-hidden-"
71    };
72
73    apiPrivate.Commands = {
74        AddAuditCategory: "addAuditCategory",
75        AddAuditResult: "addAuditResult",
76        AddConsoleMessage: "addConsoleMessage",
77        AddRequestHeaders: "addRequestHeaders",
78        ApplyStyleSheet: "applyStyleSheet",
79        CreatePanel: "createPanel",
80        CreateSidebarPane: "createSidebarPane",
81        CreateStatusBarButton: "createStatusBarButton",
82        EvaluateOnInspectedPage: "evaluateOnInspectedPage",
83        ForwardKeyboardEvent: "_forwardKeyboardEvent",
84        GetConsoleMessages: "getConsoleMessages",
85        GetHAR: "getHAR",
86        GetPageResources: "getPageResources",
87        GetRequestContent: "getRequestContent",
88        GetResourceContent: "getResourceContent",
89        InspectedURLChanged: "inspectedURLChanged",
90        OpenResource: "openResource",
91        Reload: "Reload",
92        Subscribe: "subscribe",
93        SetOpenResourceHandler: "setOpenResourceHandler",
94        SetResourceContent: "setResourceContent",
95        SetSidebarContent: "setSidebarContent",
96        SetSidebarHeight: "setSidebarHeight",
97        SetSidebarPage: "setSidebarPage",
98        ShowPanel: "showPanel",
99        StopAuditCategoryRun: "stopAuditCategoryRun",
100        Unsubscribe: "unsubscribe",
101        UpdateAuditProgress: "updateAuditProgress",
102        UpdateButton: "updateButton"
103    };
104}
105
106/**
107 * @param {number} injectedScriptId
108 * @return {!Object}
109 */
110function injectedExtensionAPI(injectedScriptId)
111{
112
113var apiPrivate = {};
114
115defineCommonExtensionSymbols(apiPrivate);
116
117var commands = apiPrivate.Commands;
118var events = apiPrivate.Events;
119var userAction = false;
120
121// Here and below, all constructors are private to API implementation.
122// For a public type Foo, if internal fields are present, these are on
123// a private FooImpl type, an instance of FooImpl is used in a closure
124// by Foo consutrctor to re-bind publicly exported members to an instance
125// of Foo.
126
127/**
128 * @constructor
129 */
130function EventSinkImpl(type, customDispatch)
131{
132    this._type = type;
133    this._listeners = [];
134    this._customDispatch = customDispatch;
135}
136
137EventSinkImpl.prototype = {
138    addListener: function(callback)
139    {
140        if (typeof callback !== "function")
141            throw "addListener: callback is not a function";
142        if (this._listeners.length === 0)
143            extensionServer.sendRequest({ command: commands.Subscribe, type: this._type });
144        this._listeners.push(callback);
145        extensionServer.registerHandler("notify-" + this._type, this._dispatch.bind(this));
146    },
147
148    removeListener: function(callback)
149    {
150        var listeners = this._listeners;
151
152        for (var i = 0; i < listeners.length; ++i) {
153            if (listeners[i] === callback) {
154                listeners.splice(i, 1);
155                break;
156            }
157        }
158        if (this._listeners.length === 0)
159            extensionServer.sendRequest({ command: commands.Unsubscribe, type: this._type });
160    },
161
162    /**
163     * @param {...} vararg
164     */
165    _fire: function(vararg)
166    {
167        var listeners = this._listeners.slice();
168        for (var i = 0; i < listeners.length; ++i)
169            listeners[i].apply(null, arguments);
170    },
171
172    _dispatch: function(request)
173    {
174         if (this._customDispatch)
175             this._customDispatch.call(this, request);
176         else
177             this._fire.apply(this, request.arguments);
178    }
179}
180
181/**
182 * @constructor
183 */
184function InspectorExtensionAPI()
185{
186    this.audits = new Audits();
187    this.inspectedWindow = new InspectedWindow();
188    this.panels = new Panels();
189    this.network = new Network();
190    defineDeprecatedProperty(this, "webInspector", "resources", "network");
191    this.console = new ConsoleAPI();
192}
193
194/**
195 * @constructor
196 */
197function ConsoleAPI()
198{
199    this.onMessageAdded = new EventSink(events.ConsoleMessageAdded);
200}
201
202ConsoleAPI.prototype = {
203    getMessages: function(callback)
204    {
205        extensionServer.sendRequest({ command: commands.GetConsoleMessages }, callback);
206    },
207
208    addMessage: function(severity, text, url, line)
209    {
210        extensionServer.sendRequest({ command: commands.AddConsoleMessage, severity: severity, text: text, url: url, line: line });
211    },
212
213    get Severity()
214    {
215        return apiPrivate.console.Severity;
216    }
217}
218
219/**
220 * @constructor
221 */
222function Network()
223{
224    /**
225     * @this {EventSinkImpl}
226     */
227    function dispatchRequestEvent(message)
228    {
229        var request = message.arguments[1];
230        request.__proto__ = new Request(message.arguments[0]);
231        this._fire(request);
232    }
233    this.onRequestFinished = new EventSink(events.NetworkRequestFinished, dispatchRequestEvent);
234    defineDeprecatedProperty(this, "network", "onFinished", "onRequestFinished");
235    this.onNavigated = new EventSink(events.InspectedURLChanged);
236}
237
238Network.prototype = {
239    getHAR: function(callback)
240    {
241        function callbackWrapper(result)
242        {
243            var entries = (result && result.entries) || [];
244            for (var i = 0; i < entries.length; ++i) {
245                entries[i].__proto__ = new Request(entries[i]._requestId);
246                delete entries[i]._requestId;
247            }
248            callback(result);
249        }
250        extensionServer.sendRequest({ command: commands.GetHAR }, callback && callbackWrapper);
251    },
252
253    addRequestHeaders: function(headers)
254    {
255        extensionServer.sendRequest({ command: commands.AddRequestHeaders, headers: headers, extensionId: window.location.hostname });
256    }
257}
258
259/**
260 * @constructor
261 */
262function RequestImpl(id)
263{
264    this._id = id;
265}
266
267RequestImpl.prototype = {
268    getContent: function(callback)
269    {
270        function callbackWrapper(response)
271        {
272            callback(response.content, response.encoding);
273        }
274        extensionServer.sendRequest({ command: commands.GetRequestContent, id: this._id }, callback && callbackWrapper);
275    }
276}
277
278/**
279 * @constructor
280 */
281function Panels()
282{
283    var panels = {
284        elements: new ElementsPanel(),
285        sources: new SourcesPanel(),
286    };
287
288    function panelGetter(name)
289    {
290        return panels[name];
291    }
292    for (var panel in panels)
293        this.__defineGetter__(panel, panelGetter.bind(null, panel));
294    this.applyStyleSheet = function(styleSheet) { extensionServer.sendRequest({ command: commands.ApplyStyleSheet, styleSheet: styleSheet }); };
295}
296
297Panels.prototype = {
298    create: function(title, icon, page, callback)
299    {
300        var id = "extension-panel-" + extensionServer.nextObjectId();
301        var request = {
302            command: commands.CreatePanel,
303            id: id,
304            title: title,
305            icon: icon,
306            page: page
307        };
308        extensionServer.sendRequest(request, callback && callback.bind(this, new ExtensionPanel(id)));
309    },
310
311    setOpenResourceHandler: function(callback)
312    {
313        var hadHandler = extensionServer.hasHandler(events.OpenResource);
314
315        function callbackWrapper(message)
316        {
317            // Allow the panel to show itself when handling the event.
318            userAction = true;
319            try {
320                callback.call(null, new Resource(message.resource), message.lineNumber);
321            } finally {
322                userAction = false;
323            }
324        }
325
326        if (!callback)
327            extensionServer.unregisterHandler(events.OpenResource);
328        else
329            extensionServer.registerHandler(events.OpenResource, callbackWrapper);
330
331        // Only send command if we either removed an existing handler or added handler and had none before.
332        if (hadHandler === !callback)
333            extensionServer.sendRequest({ command: commands.SetOpenResourceHandler, "handlerPresent": !!callback });
334    },
335
336    openResource: function(url, lineNumber, callback)
337    {
338        extensionServer.sendRequest({ command: commands.OpenResource, "url": url, "lineNumber": lineNumber }, callback);
339    },
340
341    get SearchAction()
342    {
343        return apiPrivate.panels.SearchAction;
344    }
345}
346
347/**
348 * @constructor
349 */
350function ExtensionViewImpl(id)
351{
352    this._id = id;
353
354    /**
355     * @this {EventSinkImpl}
356     */
357    function dispatchShowEvent(message)
358    {
359        var frameIndex = message.arguments[0];
360        if (typeof frameIndex === "number")
361            this._fire(window.parent.frames[frameIndex]);
362        else
363            this._fire();
364    }
365
366    if (id) {
367        this.onShown = new EventSink(events.ViewShown + id, dispatchShowEvent);
368        this.onHidden = new EventSink(events.ViewHidden + id);
369    }
370}
371
372/**
373 * @constructor
374 * @extends {ExtensionViewImpl}
375 * @param {string} hostPanelName
376 */
377function PanelWithSidebarImpl(hostPanelName)
378{
379    ExtensionViewImpl.call(this, null);
380    this._hostPanelName = hostPanelName;
381    this.onSelectionChanged = new EventSink(events.PanelObjectSelected + hostPanelName);
382}
383
384PanelWithSidebarImpl.prototype = {
385    createSidebarPane: function(title, callback)
386    {
387        var id = "extension-sidebar-" + extensionServer.nextObjectId();
388        var request = {
389            command: commands.CreateSidebarPane,
390            panel: this._hostPanelName,
391            id: id,
392            title: title
393        };
394        function callbackWrapper()
395        {
396            callback(new ExtensionSidebarPane(id));
397        }
398        extensionServer.sendRequest(request, callback && callbackWrapper);
399    },
400
401    __proto__: ExtensionViewImpl.prototype
402}
403
404function declareInterfaceClass(implConstructor)
405{
406    return function()
407    {
408        var impl = { __proto__: implConstructor.prototype };
409        implConstructor.apply(impl, arguments);
410        populateInterfaceClass(this, impl);
411    }
412}
413
414function defineDeprecatedProperty(object, className, oldName, newName)
415{
416    var warningGiven = false;
417    function getter()
418    {
419        if (!warningGiven) {
420            console.warn(className + "." + oldName + " is deprecated. Use " + className + "." + newName + " instead");
421            warningGiven = true;
422        }
423        return object[newName];
424    }
425    object.__defineGetter__(oldName, getter);
426}
427
428function extractCallbackArgument(args)
429{
430    var lastArgument = args[args.length - 1];
431    return typeof lastArgument === "function" ? lastArgument : undefined;
432}
433
434var AuditCategory = declareInterfaceClass(AuditCategoryImpl);
435var AuditResult = declareInterfaceClass(AuditResultImpl);
436var Button = declareInterfaceClass(ButtonImpl);
437var EventSink = declareInterfaceClass(EventSinkImpl);
438var ExtensionPanel = declareInterfaceClass(ExtensionPanelImpl);
439var ExtensionSidebarPane = declareInterfaceClass(ExtensionSidebarPaneImpl);
440var PanelWithSidebar = declareInterfaceClass(PanelWithSidebarImpl);
441var Request = declareInterfaceClass(RequestImpl);
442var Resource = declareInterfaceClass(ResourceImpl);
443
444/**
445 * @constructor
446 * @extends {PanelWithSidebar}
447 */
448function ElementsPanel()
449{
450    PanelWithSidebar.call(this, "elements");
451}
452
453ElementsPanel.prototype = {
454    __proto__: PanelWithSidebar.prototype
455}
456
457/**
458 * @constructor
459 * @extends {PanelWithSidebar}
460 */
461function SourcesPanel()
462{
463    PanelWithSidebar.call(this, "sources");
464}
465
466SourcesPanel.prototype = {
467    __proto__: PanelWithSidebar.prototype
468}
469
470/**
471 * @constructor
472 * @extends {ExtensionViewImpl}
473 */
474function ExtensionPanelImpl(id)
475{
476    ExtensionViewImpl.call(this, id);
477    this.onSearch = new EventSink(events.PanelSearch + id);
478}
479
480ExtensionPanelImpl.prototype = {
481    /**
482     * @return {!Object}
483     */
484    createStatusBarButton: function(iconPath, tooltipText, disabled)
485    {
486        var id = "button-" + extensionServer.nextObjectId();
487        var request = {
488            command: commands.CreateStatusBarButton,
489            panel: this._id,
490            id: id,
491            icon: iconPath,
492            tooltip: tooltipText,
493            disabled: !!disabled
494        };
495        extensionServer.sendRequest(request);
496        return new Button(id);
497    },
498
499    show: function()
500    {
501        if (!userAction)
502            return;
503
504        var request = {
505            command: commands.ShowPanel,
506            id: this._id
507        };
508        extensionServer.sendRequest(request);
509    },
510
511    __proto__: ExtensionViewImpl.prototype
512}
513
514/**
515 * @constructor
516 * @extends {ExtensionViewImpl}
517 */
518function ExtensionSidebarPaneImpl(id)
519{
520    ExtensionViewImpl.call(this, id);
521}
522
523ExtensionSidebarPaneImpl.prototype = {
524    setHeight: function(height)
525    {
526        extensionServer.sendRequest({ command: commands.SetSidebarHeight, id: this._id, height: height });
527    },
528
529    setExpression: function(expression, rootTitle, evaluateOptions)
530    {
531        var request = {
532            command: commands.SetSidebarContent,
533            id: this._id,
534            expression: expression,
535            rootTitle: rootTitle,
536            evaluateOnPage: true,
537        };
538        if (typeof evaluateOptions === "object")
539            request.evaluateOptions = evaluateOptions;
540        extensionServer.sendRequest(request, extractCallbackArgument(arguments));
541    },
542
543    setObject: function(jsonObject, rootTitle, callback)
544    {
545        extensionServer.sendRequest({ command: commands.SetSidebarContent, id: this._id, expression: jsonObject, rootTitle: rootTitle }, callback);
546    },
547
548    setPage: function(page)
549    {
550        extensionServer.sendRequest({ command: commands.SetSidebarPage, id: this._id, page: page });
551    },
552
553    __proto__: ExtensionViewImpl.prototype
554}
555
556/**
557 * @constructor
558 */
559function ButtonImpl(id)
560{
561    this._id = id;
562    this.onClicked = new EventSink(events.ButtonClicked + id);
563}
564
565ButtonImpl.prototype = {
566    update: function(iconPath, tooltipText, disabled)
567    {
568        var request = {
569            command: commands.UpdateButton,
570            id: this._id,
571            icon: iconPath,
572            tooltip: tooltipText,
573            disabled: !!disabled
574        };
575        extensionServer.sendRequest(request);
576    }
577};
578
579/**
580 * @constructor
581 */
582function Audits()
583{
584}
585
586Audits.prototype = {
587    /**
588     * @return {!AuditCategory}
589     */
590    addCategory: function(displayName, resultCount)
591    {
592        var id = "extension-audit-category-" + extensionServer.nextObjectId();
593        if (typeof resultCount !== "undefined")
594            console.warn("Passing resultCount to audits.addCategory() is deprecated. Use AuditResult.updateProgress() instead.");
595        extensionServer.sendRequest({ command: commands.AddAuditCategory, id: id, displayName: displayName, resultCount: resultCount });
596        return new AuditCategory(id);
597    }
598}
599
600/**
601 * @constructor
602 */
603function AuditCategoryImpl(id)
604{
605    /**
606     * @this {EventSinkImpl}
607     */
608    function dispatchAuditEvent(request)
609    {
610        var auditResult = new AuditResult(request.arguments[0]);
611        try {
612            this._fire(auditResult);
613        } catch (e) {
614            console.error("Uncaught exception in extension audit event handler: " + e);
615            auditResult.done();
616        }
617    }
618    this._id = id;
619    this.onAuditStarted = new EventSink(events.AuditStarted + id, dispatchAuditEvent);
620}
621
622/**
623 * @constructor
624 */
625function AuditResultImpl(id)
626{
627    this._id = id;
628
629    this.createURL = this._nodeFactory.bind(this, "url");
630    this.createSnippet = this._nodeFactory.bind(this, "snippet");
631    this.createText = this._nodeFactory.bind(this, "text");
632    this.createObject = this._nodeFactory.bind(this, "object");
633    this.createNode = this._nodeFactory.bind(this, "node");
634}
635
636AuditResultImpl.prototype = {
637    addResult: function(displayName, description, severity, details)
638    {
639        // shorthand for specifying details directly in addResult().
640        if (details && !(details instanceof AuditResultNode))
641            details = new AuditResultNode(details instanceof Array ? details : [details]);
642
643        var request = {
644            command: commands.AddAuditResult,
645            resultId: this._id,
646            displayName: displayName,
647            description: description,
648            severity: severity,
649            details: details
650        };
651        extensionServer.sendRequest(request);
652    },
653
654    /**
655     * @return {!Object}
656     */
657    createResult: function()
658    {
659        return new AuditResultNode(Array.prototype.slice.call(arguments));
660    },
661
662    updateProgress: function(worked, totalWork)
663    {
664        extensionServer.sendRequest({ command: commands.UpdateAuditProgress, resultId: this._id, progress: worked / totalWork });
665    },
666
667    done: function()
668    {
669        extensionServer.sendRequest({ command: commands.StopAuditCategoryRun, resultId: this._id });
670    },
671
672    /**
673     * @type {!Object.<string, string>}
674     */
675    get Severity()
676    {
677        return apiPrivate.audits.Severity;
678    },
679
680    /**
681     * @return {!{type: string, arguments: !Array.<string|number>}}
682     */
683    createResourceLink: function(url, lineNumber)
684    {
685        return {
686            type: "resourceLink",
687            arguments: [url, lineNumber && lineNumber - 1]
688        };
689    },
690
691    /**
692     * @return {!{type: string, arguments: !Array.<string|number>}}
693     */
694    _nodeFactory: function(type)
695    {
696        return {
697            type: type,
698            arguments: Array.prototype.slice.call(arguments, 1)
699        };
700    }
701}
702
703/**
704 * @constructor
705 */
706function AuditResultNode(contents)
707{
708    this.contents = contents;
709    this.children = [];
710    this.expanded = false;
711}
712
713AuditResultNode.prototype = {
714    /**
715     * @return {!Object}
716     */
717    addChild: function()
718    {
719        var node = new AuditResultNode(Array.prototype.slice.call(arguments));
720        this.children.push(node);
721        return node;
722    }
723};
724
725/**
726 * @constructor
727 */
728function InspectedWindow()
729{
730    /**
731     * @this {EventSinkImpl}
732     */
733    function dispatchResourceEvent(message)
734    {
735        this._fire(new Resource(message.arguments[0]));
736    }
737
738    /**
739     * @this {EventSinkImpl}
740     */
741    function dispatchResourceContentEvent(message)
742    {
743        this._fire(new Resource(message.arguments[0]), message.arguments[1]);
744    }
745
746    this.onResourceAdded = new EventSink(events.ResourceAdded, dispatchResourceEvent);
747    this.onResourceContentCommitted = new EventSink(events.ResourceContentCommitted, dispatchResourceContentEvent);
748}
749
750InspectedWindow.prototype = {
751    reload: function(optionsOrUserAgent)
752    {
753        var options = null;
754        if (typeof optionsOrUserAgent === "object")
755            options = optionsOrUserAgent;
756        else if (typeof optionsOrUserAgent === "string") {
757            options = { userAgent: optionsOrUserAgent };
758            console.warn("Passing userAgent as string parameter to inspectedWindow.reload() is deprecated. " +
759                         "Use inspectedWindow.reload({ userAgent: value}) instead.");
760        }
761        extensionServer.sendRequest({ command: commands.Reload, options: options });
762    },
763
764    /**
765     * @return {?Object}
766     */
767    eval: function(expression, evaluateOptions)
768    {
769        var callback = extractCallbackArgument(arguments);
770        function callbackWrapper(result)
771        {
772            if (result.isError || result.isException)
773                callback(undefined, result);
774            else
775                callback(result.value);
776        }
777        var request = {
778            command: commands.EvaluateOnInspectedPage,
779            expression: expression
780        };
781        if (typeof evaluateOptions === "object")
782            request.evaluateOptions = evaluateOptions;
783        extensionServer.sendRequest(request, callback && callbackWrapper);
784        return null;
785    },
786
787    getResources: function(callback)
788    {
789        function wrapResource(resourceData)
790        {
791            return new Resource(resourceData);
792        }
793        function callbackWrapper(resources)
794        {
795            callback(resources.map(wrapResource));
796        }
797        extensionServer.sendRequest({ command: commands.GetPageResources }, callback && callbackWrapper);
798    }
799}
800
801/**
802 * @constructor
803 */
804function ResourceImpl(resourceData)
805{
806    this._url = resourceData.url
807    this._type = resourceData.type;
808}
809
810ResourceImpl.prototype = {
811    get url()
812    {
813        return this._url;
814    },
815
816    get type()
817    {
818        return this._type;
819    },
820
821    getContent: function(callback)
822    {
823        function callbackWrapper(response)
824        {
825            callback(response.content, response.encoding);
826        }
827
828        extensionServer.sendRequest({ command: commands.GetResourceContent, url: this._url }, callback && callbackWrapper);
829    },
830
831    setContent: function(content, commit, callback)
832    {
833        extensionServer.sendRequest({ command: commands.SetResourceContent, url: this._url, content: content, commit: commit }, callback);
834    }
835}
836
837var keyboardEventRequestQueue = [];
838var forwardTimer = null;
839
840function forwardKeyboardEvent(event)
841{
842    const Esc = "U+001B";
843    // We only care about global hotkeys, not about random text
844    if (!event.ctrlKey && !event.altKey && !event.metaKey && !/^F\d+$/.test(event.keyIdentifier) && event.keyIdentifier !== Esc)
845        return;
846    var requestPayload = {
847        eventType: event.type,
848        ctrlKey: event.ctrlKey,
849        altKey: event.altKey,
850        metaKey: event.metaKey,
851        keyIdentifier: event.keyIdentifier,
852        location: event.location,
853        keyCode: event.keyCode
854    };
855    keyboardEventRequestQueue.push(requestPayload);
856    if (!forwardTimer)
857        forwardTimer = setTimeout(forwardEventQueue, 0);
858}
859
860function forwardEventQueue()
861{
862    forwardTimer = null;
863    var request = {
864        command: commands.ForwardKeyboardEvent,
865        entries: keyboardEventRequestQueue
866    };
867    extensionServer.sendRequest(request);
868    keyboardEventRequestQueue = [];
869}
870
871document.addEventListener("keydown", forwardKeyboardEvent, false);
872document.addEventListener("keypress", forwardKeyboardEvent, false);
873
874/**
875 * @constructor
876 */
877function ExtensionServerClient()
878{
879    this._callbacks = {};
880    this._handlers = {};
881    this._lastRequestId = 0;
882    this._lastObjectId = 0;
883
884    this.registerHandler("callback", this._onCallback.bind(this));
885
886    var channel = new MessageChannel();
887    this._port = channel.port1;
888    this._port.addEventListener("message", this._onMessage.bind(this), false);
889    this._port.start();
890
891    window.parent.postMessage("registerExtension", [ channel.port2 ], "*");
892}
893
894ExtensionServerClient.prototype = {
895    /**
896     * @param {!Object} message
897     * @param {function()=} callback
898     */
899    sendRequest: function(message, callback)
900    {
901        if (typeof callback === "function")
902            message.requestId = this._registerCallback(callback);
903        this._port.postMessage(message);
904    },
905
906    /**
907     * @return {boolean}
908     */
909    hasHandler: function(command)
910    {
911        return !!this._handlers[command];
912    },
913
914    registerHandler: function(command, handler)
915    {
916        this._handlers[command] = handler;
917    },
918
919    unregisterHandler: function(command)
920    {
921        delete this._handlers[command];
922    },
923
924    /**
925     * @return {string}
926     */
927    nextObjectId: function()
928    {
929        return injectedScriptId + "_" + ++this._lastObjectId;
930    },
931
932    _registerCallback: function(callback)
933    {
934        var id = ++this._lastRequestId;
935        this._callbacks[id] = callback;
936        return id;
937    },
938
939    _onCallback: function(request)
940    {
941        if (request.requestId in this._callbacks) {
942            var callback = this._callbacks[request.requestId];
943            delete this._callbacks[request.requestId];
944            callback(request.result);
945        }
946    },
947
948    _onMessage: function(event)
949    {
950        var request = event.data;
951        var handler = this._handlers[request.command];
952        if (handler)
953            handler.call(this, request);
954    }
955}
956
957function populateInterfaceClass(interfaze, implementation)
958{
959    for (var member in implementation) {
960        if (member.charAt(0) === "_")
961            continue;
962        var descriptor = null;
963        // Traverse prototype chain until we find the owner.
964        for (var owner = implementation; owner && !descriptor; owner = owner.__proto__)
965            descriptor = Object.getOwnPropertyDescriptor(owner, member);
966        if (!descriptor)
967            continue;
968        if (typeof descriptor.value === "function")
969            interfaze[member] = descriptor.value.bind(implementation);
970        else if (typeof descriptor.get === "function")
971            interfaze.__defineGetter__(member, descriptor.get.bind(implementation));
972        else
973            Object.defineProperty(interfaze, member, descriptor);
974    }
975}
976
977// extensionServer is a closure variable defined by the glue below -- make sure we fail if it's not there.
978if (!extensionServer)
979    extensionServer = new ExtensionServerClient();
980
981return new InspectorExtensionAPI();
982}
983
984/**
985 * @suppress {checkVars, checkTypes}
986 */
987function platformExtensionAPI(coreAPI)
988{
989    function getTabId()
990    {
991        return tabId;
992    }
993    chrome = window.chrome || {};
994    // Override chrome.devtools as a workaround for a error-throwing getter being exposed
995    // in extension pages loaded into a non-extension process (only happens for remote client
996    // extensions)
997    var devtools_descriptor = Object.getOwnPropertyDescriptor(chrome, "devtools");
998    if (!devtools_descriptor || devtools_descriptor.get)
999        Object.defineProperty(chrome, "devtools", { value: {}, enumerable: true });
1000    // Only expose tabId on chrome.devtools.inspectedWindow, not webInspector.inspectedWindow.
1001    chrome.devtools.inspectedWindow = {};
1002    chrome.devtools.inspectedWindow.__defineGetter__("tabId", getTabId);
1003    chrome.devtools.inspectedWindow.__proto__ = coreAPI.inspectedWindow;
1004    chrome.devtools.network = coreAPI.network;
1005    chrome.devtools.panels = coreAPI.panels;
1006
1007    // default to expose experimental APIs for now.
1008    if (extensionInfo.exposeExperimentalAPIs !== false) {
1009        chrome.experimental = chrome.experimental || {};
1010        chrome.experimental.devtools = chrome.experimental.devtools || {};
1011
1012        var properties = Object.getOwnPropertyNames(coreAPI);
1013        for (var i = 0; i < properties.length; ++i) {
1014            var descriptor = Object.getOwnPropertyDescriptor(coreAPI, properties[i]);
1015            Object.defineProperty(chrome.experimental.devtools, properties[i], descriptor);
1016        }
1017        chrome.experimental.devtools.inspectedWindow = chrome.devtools.inspectedWindow;
1018    }
1019    if (extensionInfo.exposeWebInspectorNamespace)
1020        window.webInspector = coreAPI;
1021}
1022
1023/**
1024 * @param {!ExtensionDescriptor} extensionInfo
1025 * @return {string}
1026 */
1027function buildPlatformExtensionAPI(extensionInfo)
1028{
1029    return "var extensionInfo = " + JSON.stringify(extensionInfo) + ";" +
1030       "var tabId = " + WebInspector._inspectedTabId + ";" +
1031       platformExtensionAPI.toString();
1032}
1033
1034/**
1035 * @param {!ExtensionDescriptor} extensionInfo
1036 * @return {string}
1037 */
1038function buildExtensionAPIInjectedScript(extensionInfo)
1039{
1040    return "(function(injectedScriptId){ " +
1041        "var extensionServer;" +
1042        defineCommonExtensionSymbols.toString() + ";" +
1043        injectedExtensionAPI.toString() + ";" +
1044        buildPlatformExtensionAPI(extensionInfo) + ";" +
1045        "platformExtensionAPI(injectedExtensionAPI(injectedScriptId));" +
1046        "return {};" +
1047        "})";
1048}
1049