• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2011 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
31/**
32 * @constructor
33 * @implements {WebInspector.ExtensionServerAPI}
34 */
35WebInspector.ExtensionServer = function()
36{
37    this._clientObjects = {};
38    this._handlers = {};
39    this._subscribers = {};
40    this._subscriptionStartHandlers = {};
41    this._subscriptionStopHandlers = {};
42    this._extraHeaders = {};
43    this._requests = {};
44    this._lastRequestId = 0;
45    this._registeredExtensions = {};
46    this._status = new WebInspector.ExtensionStatus();
47
48    var commands = WebInspector.extensionAPI.Commands;
49
50    this._registerHandler(commands.AddAuditCategory, this._onAddAuditCategory.bind(this));
51    this._registerHandler(commands.AddAuditResult, this._onAddAuditResult.bind(this));
52    this._registerHandler(commands.AddConsoleMessage, this._onAddConsoleMessage.bind(this));
53    this._registerHandler(commands.AddRequestHeaders, this._onAddRequestHeaders.bind(this));
54    this._registerHandler(commands.ApplyStyleSheet, this._onApplyStyleSheet.bind(this));
55    this._registerHandler(commands.CreatePanel, this._onCreatePanel.bind(this));
56    this._registerHandler(commands.CreateSidebarPane, this._onCreateSidebarPane.bind(this));
57    this._registerHandler(commands.CreateStatusBarButton, this._onCreateStatusBarButton.bind(this));
58    this._registerHandler(commands.EvaluateOnInspectedPage, this._onEvaluateOnInspectedPage.bind(this));
59    this._registerHandler(commands.ForwardKeyboardEvent, this._onForwardKeyboardEvent.bind(this));
60    this._registerHandler(commands.GetHAR, this._onGetHAR.bind(this));
61    this._registerHandler(commands.GetConsoleMessages, this._onGetConsoleMessages.bind(this));
62    this._registerHandler(commands.GetPageResources, this._onGetPageResources.bind(this));
63    this._registerHandler(commands.GetRequestContent, this._onGetRequestContent.bind(this));
64    this._registerHandler(commands.GetResourceContent, this._onGetResourceContent.bind(this));
65    this._registerHandler(commands.Reload, this._onReload.bind(this));
66    this._registerHandler(commands.SetOpenResourceHandler, this._onSetOpenResourceHandler.bind(this));
67    this._registerHandler(commands.SetResourceContent, this._onSetResourceContent.bind(this));
68    this._registerHandler(commands.SetSidebarHeight, this._onSetSidebarHeight.bind(this));
69    this._registerHandler(commands.SetSidebarContent, this._onSetSidebarContent.bind(this));
70    this._registerHandler(commands.SetSidebarPage, this._onSetSidebarPage.bind(this));
71    this._registerHandler(commands.ShowPanel, this._onShowPanel.bind(this));
72    this._registerHandler(commands.StopAuditCategoryRun, this._onStopAuditCategoryRun.bind(this));
73    this._registerHandler(commands.Subscribe, this._onSubscribe.bind(this));
74    this._registerHandler(commands.OpenResource, this._onOpenResource.bind(this));
75    this._registerHandler(commands.Unsubscribe, this._onUnsubscribe.bind(this));
76    this._registerHandler(commands.UpdateButton, this._onUpdateButton.bind(this));
77    this._registerHandler(commands.UpdateAuditProgress, this._onUpdateAuditProgress.bind(this));
78    window.addEventListener("message", this._onWindowMessage.bind(this), false);
79
80    this._initExtensions();
81}
82
83WebInspector.ExtensionServer.prototype = {
84    /**
85     * @return {boolean}
86     */
87    hasExtensions: function()
88    {
89        return !!Object.keys(this._registeredExtensions).length;
90    },
91
92    notifySearchAction: function(panelId, action, searchString)
93    {
94        this._postNotification(WebInspector.extensionAPI.Events.PanelSearch + panelId, action, searchString);
95    },
96
97    notifyViewShown: function(identifier, frameIndex)
98    {
99        this._postNotification(WebInspector.extensionAPI.Events.ViewShown + identifier, frameIndex);
100    },
101
102    notifyViewHidden: function(identifier)
103    {
104        this._postNotification(WebInspector.extensionAPI.Events.ViewHidden + identifier);
105    },
106
107    notifyButtonClicked: function(identifier)
108    {
109        this._postNotification(WebInspector.extensionAPI.Events.ButtonClicked + identifier);
110    },
111
112    _inspectedURLChanged: function(event)
113    {
114        this._requests = {};
115        var url = event.data;
116        this._postNotification(WebInspector.extensionAPI.Events.InspectedURLChanged, url);
117    },
118
119    startAuditRun: function(category, auditRun)
120    {
121        this._clientObjects[auditRun.id] = auditRun;
122        this._postNotification("audit-started-" + category.id, auditRun.id);
123    },
124
125    stopAuditRun: function(auditRun)
126    {
127        delete this._clientObjects[auditRun.id];
128    },
129
130    /**
131     * @param {string} type
132     * @return {boolean}
133     */
134    hasSubscribers: function(type)
135    {
136        return !!this._subscribers[type];
137    },
138
139    /**
140     * @param {string} type
141     * @param {...*} vararg
142     */
143    _postNotification: function(type, vararg)
144    {
145        var subscribers = this._subscribers[type];
146        if (!subscribers)
147            return;
148        var message = {
149            command: "notify-" + type,
150            arguments: Array.prototype.slice.call(arguments, 1)
151        };
152        for (var i = 0; i < subscribers.length; ++i)
153            subscribers[i].postMessage(message);
154    },
155
156    _onSubscribe: function(message, port)
157    {
158        var subscribers = this._subscribers[message.type];
159        if (subscribers)
160            subscribers.push(port);
161        else {
162            this._subscribers[message.type] = [ port ];
163            if (this._subscriptionStartHandlers[message.type])
164                this._subscriptionStartHandlers[message.type]();
165        }
166    },
167
168    _onUnsubscribe: function(message, port)
169    {
170        var subscribers = this._subscribers[message.type];
171        if (!subscribers)
172            return;
173        subscribers.remove(port);
174        if (!subscribers.length) {
175            delete this._subscribers[message.type];
176            if (this._subscriptionStopHandlers[message.type])
177                this._subscriptionStopHandlers[message.type]();
178        }
179    },
180
181    _onAddRequestHeaders: function(message)
182    {
183        var id = message.extensionId;
184        if (typeof id !== "string")
185            return this._status.E_BADARGTYPE("extensionId", typeof id, "string");
186        var extensionHeaders = this._extraHeaders[id];
187        if (!extensionHeaders) {
188            extensionHeaders = {};
189            this._extraHeaders[id] = extensionHeaders;
190        }
191        for (var name in message.headers)
192            extensionHeaders[name] = message.headers[name];
193        var allHeaders = /** @type {!NetworkAgent.Headers} */ ({});
194        for (var extension in this._extraHeaders) {
195            var headers = this._extraHeaders[extension];
196            for (name in headers) {
197                if (typeof headers[name] === "string")
198                    allHeaders[name] = headers[name];
199            }
200        }
201        NetworkAgent.setExtraHTTPHeaders(allHeaders);
202    },
203
204    _onApplyStyleSheet: function(message)
205    {
206        if (!Runtime.experiments.isEnabled("applyCustomStylesheet"))
207            return;
208        var styleSheet = document.createElement("style");
209        styleSheet.textContent = message.styleSheet;
210        document.head.appendChild(styleSheet);
211    },
212
213    _onCreatePanel: function(message, port)
214    {
215        var id = message.id;
216        // The ids are generated on the client API side and must be unique, so the check below
217        // shouldn't be hit unless someone is bypassing the API.
218        if (id in this._clientObjects || WebInspector.inspectorView.hasPanel(id))
219            return this._status.E_EXISTS(id);
220
221        var page = this._expandResourcePath(port._extensionOrigin, message.page);
222        var panelDescriptor = new WebInspector.ExtensionServerPanelDescriptor(id, message.title, new WebInspector.ExtensionPanel(id, page));
223        this._clientObjects[id] = panelDescriptor.panel();
224        WebInspector.inspectorView.addPanel(panelDescriptor);
225        return this._status.OK();
226    },
227
228    _onShowPanel: function(message)
229    {
230        // Note: WebInspector.inspectorView.showPanel already sanitizes input.
231        WebInspector.inspectorView.showPanel(message.id);
232    },
233
234    _onCreateStatusBarButton: function(message, port)
235    {
236        var panel = this._clientObjects[message.panel];
237        if (!panel || !(panel instanceof WebInspector.ExtensionPanel))
238            return this._status.E_NOTFOUND(message.panel);
239        var button = new WebInspector.ExtensionButton(message.id, this._expandResourcePath(port._extensionOrigin, message.icon), message.tooltip, message.disabled);
240        this._clientObjects[message.id] = button;
241        panel.addStatusBarItem(button.element);
242        return this._status.OK();
243    },
244
245    _onUpdateButton: function(message, port)
246    {
247        var button = this._clientObjects[message.id];
248        if (!button || !(button instanceof WebInspector.ExtensionButton))
249            return this._status.E_NOTFOUND(message.id);
250        button.update(this._expandResourcePath(port._extensionOrigin, message.icon), message.tooltip, message.disabled);
251        return this._status.OK();
252    },
253
254    _onCreateSidebarPane: function(message)
255    {
256        var panel = WebInspector.inspectorView.panel(message.panel);
257        if (!panel)
258            return this._status.E_NOTFOUND(message.panel);
259        if (!panel.addExtensionSidebarPane)
260            return this._status.E_NOTSUPPORTED();
261        var id = message.id;
262        var sidebar = new WebInspector.ExtensionSidebarPane(message.title, id);
263        this._clientObjects[id] = sidebar;
264        panel.addExtensionSidebarPane(id, sidebar);
265
266        return this._status.OK();
267    },
268
269    _onSetSidebarHeight: function(message)
270    {
271        var sidebar = this._clientObjects[message.id];
272        if (!sidebar)
273            return this._status.E_NOTFOUND(message.id);
274        sidebar.setHeight(message.height);
275        return this._status.OK();
276    },
277
278    _onSetSidebarContent: function(message, port)
279    {
280        var sidebar = this._clientObjects[message.id];
281        if (!sidebar)
282            return this._status.E_NOTFOUND(message.id);
283
284        /**
285         * @this {WebInspector.ExtensionServer}
286         */
287        function callback(error)
288        {
289            var result = error ? this._status.E_FAILED(error) : this._status.OK();
290            this._dispatchCallback(message.requestId, port, result);
291        }
292        if (message.evaluateOnPage)
293            return sidebar.setExpression(message.expression, message.rootTitle, message.evaluateOptions, port._extensionOrigin, callback.bind(this));
294        sidebar.setObject(message.expression, message.rootTitle, callback.bind(this));
295    },
296
297    _onSetSidebarPage: function(message, port)
298    {
299        var sidebar = this._clientObjects[message.id];
300        if (!sidebar)
301            return this._status.E_NOTFOUND(message.id);
302        sidebar.setPage(this._expandResourcePath(port._extensionOrigin, message.page));
303    },
304
305    _onOpenResource: function(message)
306    {
307        var uiSourceCode = WebInspector.workspace.uiSourceCodeForURL(message.url);
308        if (uiSourceCode) {
309            WebInspector.Revealer.reveal(uiSourceCode.uiLocation(message.lineNumber, 0));
310            return this._status.OK();
311        }
312
313        var resource = WebInspector.resourceForURL(message.url);
314        if (resource) {
315            WebInspector.Revealer.reveal(resource, message.lineNumber);
316            return this._status.OK();
317        }
318
319        var request = WebInspector.networkLog.requestForURL(message.url);
320        if (request) {
321            WebInspector.Revealer.reveal(request);
322            return this._status.OK();
323        }
324
325        return this._status.E_NOTFOUND(message.url);
326    },
327
328    _onSetOpenResourceHandler: function(message, port)
329    {
330        var name = this._registeredExtensions[port._extensionOrigin].name || ("Extension " + port._extensionOrigin);
331        if (message.handlerPresent)
332            WebInspector.openAnchorLocationRegistry.registerHandler(name, this._handleOpenURL.bind(this, port));
333        else
334            WebInspector.openAnchorLocationRegistry.unregisterHandler(name);
335    },
336
337    _handleOpenURL: function(port, details)
338    {
339        var url = /** @type {string} */ (details.url);
340        var contentProvider = WebInspector.workspace.uiSourceCodeForOriginURL(url) || WebInspector.resourceForURL(url);
341        if (!contentProvider)
342            return false;
343
344        var lineNumber = details.lineNumber;
345        if (typeof lineNumber === "number")
346            lineNumber += 1;
347        port.postMessage({
348            command: "open-resource",
349            resource: this._makeResource(contentProvider),
350            lineNumber: lineNumber
351        });
352        return true;
353    },
354
355    _onReload: function(message)
356    {
357        var options = /** @type {!ExtensionReloadOptions} */ (message.options || {});
358        NetworkAgent.setUserAgentOverride(typeof options.userAgent === "string" ? options.userAgent : "");
359        var injectedScript;
360        if (options.injectedScript)
361            injectedScript = "(function(){" + options.injectedScript + "})()";
362        var preprocessingScript = options.preprocessingScript;
363        WebInspector.resourceTreeModel.reloadPage(!!options.ignoreCache, injectedScript, preprocessingScript);
364        return this._status.OK();
365    },
366
367    _onEvaluateOnInspectedPage: function(message, port)
368    {
369        /**
370         * @param {?Protocol.Error} error
371         * @param {?RuntimeAgent.RemoteObject} resultPayload
372         * @param {boolean=} wasThrown
373         * @this {WebInspector.ExtensionServer}
374         */
375        function callback(error, resultPayload, wasThrown)
376        {
377            var result;
378            if (error || !resultPayload)
379                result = this._status.E_PROTOCOLERROR(error.toString());
380            else if (wasThrown)
381                result = { isException: true, value: resultPayload.description };
382            else
383                result = { value: resultPayload.value };
384
385            this._dispatchCallback(message.requestId, port, result);
386        }
387        return this.evaluate(message.expression, true, true, message.evaluateOptions, port._extensionOrigin, callback.bind(this));
388    },
389
390    _onGetConsoleMessages: function()
391    {
392        return WebInspector.multitargetConsoleModel.messages().map(this._makeConsoleMessage);
393    },
394
395    _onAddConsoleMessage: function(message)
396    {
397        function convertSeverity(level)
398        {
399            switch (level) {
400                case WebInspector.extensionAPI.console.Severity.Log:
401                    return WebInspector.ConsoleMessage.MessageLevel.Log;
402                case WebInspector.extensionAPI.console.Severity.Warning:
403                    return WebInspector.ConsoleMessage.MessageLevel.Warning;
404                case WebInspector.extensionAPI.console.Severity.Error:
405                    return WebInspector.ConsoleMessage.MessageLevel.Error;
406                case WebInspector.extensionAPI.console.Severity.Debug:
407                    return WebInspector.ConsoleMessage.MessageLevel.Debug;
408            }
409        }
410        var level = convertSeverity(message.severity);
411        if (!level)
412            return this._status.E_BADARG("message.severity", message.severity);
413
414        var mainTarget = WebInspector.targetManager.mainTarget();
415        var consoleMessage = new WebInspector.ConsoleMessage(
416            mainTarget,
417            WebInspector.ConsoleMessage.MessageSource.JS,
418            level,
419            message.text,
420            WebInspector.ConsoleMessage.MessageType.Log,
421            message.url,
422            message.line);
423        mainTarget.consoleModel.addMessage(consoleMessage);
424    },
425
426    _makeConsoleMessage: function(message)
427    {
428        function convertLevel(level)
429        {
430            if (!level)
431                return;
432            switch (level) {
433                case WebInspector.ConsoleMessage.MessageLevel.Log:
434                    return WebInspector.extensionAPI.console.Severity.Log;
435                case WebInspector.ConsoleMessage.MessageLevel.Warning:
436                    return WebInspector.extensionAPI.console.Severity.Warning;
437                case WebInspector.ConsoleMessage.MessageLevel.Error:
438                    return WebInspector.extensionAPI.console.Severity.Error;
439                case WebInspector.ConsoleMessage.MessageLevel.Debug:
440                    return WebInspector.extensionAPI.console.Severity.Debug;
441                default:
442                    return WebInspector.extensionAPI.console.Severity.Log;
443            }
444        }
445        var result = {
446            severity: convertLevel(message.level),
447            text: message.messageText,
448        };
449        if (message.url)
450            result.url = message.url;
451        if (message.line)
452            result.line = message.line;
453        return result;
454    },
455
456    _onGetHAR: function()
457    {
458        // Wake up the "network" module for HAR operations.
459        WebInspector.inspectorView.panel("network");
460        var requests = WebInspector.networkLog.requests;
461        var harLog = (new WebInspector.HARLog(requests)).build();
462        for (var i = 0; i < harLog.entries.length; ++i)
463            harLog.entries[i]._requestId = this._requestId(requests[i]);
464        return harLog;
465    },
466
467    /**
468     * @param {!WebInspector.ContentProvider} contentProvider
469     */
470    _makeResource: function(contentProvider)
471    {
472        return {
473            url: contentProvider.contentURL(),
474            type: contentProvider.contentType().name()
475        };
476    },
477
478    /**
479     * @return {!Array.<!WebInspector.ContentProvider>}
480     */
481    _onGetPageResources: function()
482    {
483        var resources = {};
484
485        /**
486         * @this {WebInspector.ExtensionServer}
487         */
488        function pushResourceData(contentProvider)
489        {
490            if (!resources[contentProvider.contentURL()])
491                resources[contentProvider.contentURL()] = this._makeResource(contentProvider);
492        }
493        var uiSourceCodes = WebInspector.workspace.uiSourceCodesForProjectType(WebInspector.projectTypes.Network);
494        uiSourceCodes = uiSourceCodes.concat(WebInspector.workspace.uiSourceCodesForProjectType(WebInspector.projectTypes.ContentScripts));
495        uiSourceCodes.forEach(pushResourceData.bind(this));
496        WebInspector.resourceTreeModel.forAllResources(pushResourceData.bind(this));
497        return Object.values(resources);
498    },
499
500    /**
501     * @param {!WebInspector.ContentProvider} contentProvider
502     * @param {!Object} message
503     * @param {!MessagePort} port
504     */
505    _getResourceContent: function(contentProvider, message, port)
506    {
507        /**
508         * @param {?string} content
509         * @this {WebInspector.ExtensionServer}
510         */
511        function onContentAvailable(content)
512        {
513            var response = {
514                encoding: (content === null) || contentProvider.contentType().isTextType() ? "" : "base64",
515                content: content
516            };
517            this._dispatchCallback(message.requestId, port, response);
518        }
519
520        contentProvider.requestContent(onContentAvailable.bind(this));
521    },
522
523    _onGetRequestContent: function(message, port)
524    {
525        var request = this._requestById(message.id);
526        if (!request)
527            return this._status.E_NOTFOUND(message.id);
528        this._getResourceContent(request, message, port);
529    },
530
531    _onGetResourceContent: function(message, port)
532    {
533        var url = /** @type {string} */ (message.url);
534        var contentProvider = WebInspector.workspace.uiSourceCodeForOriginURL(url) || WebInspector.resourceForURL(url);
535        if (!contentProvider)
536            return this._status.E_NOTFOUND(url);
537        this._getResourceContent(contentProvider, message, port);
538    },
539
540    _onSetResourceContent: function(message, port)
541    {
542        /**
543         * @param {?Protocol.Error} error
544         * @this {WebInspector.ExtensionServer}
545         */
546        function callbackWrapper(error)
547        {
548            var response = error ? this._status.E_FAILED(error) : this._status.OK();
549            this._dispatchCallback(message.requestId, port, response);
550        }
551
552        var url = /** @type {string} */ (message.url);
553        var uiSourceCode = WebInspector.workspace.uiSourceCodeForOriginURL(url);
554        if (!uiSourceCode) {
555            var resource = WebInspector.resourceTreeModel.resourceForURL(url);
556            if (!resource)
557                return this._status.E_NOTFOUND(url);
558            return this._status.E_NOTSUPPORTED("Resource is not editable")
559        }
560        uiSourceCode.setWorkingCopy(message.content);
561        if (message.commit)
562            uiSourceCode.commitWorkingCopy();
563        callbackWrapper.call(this, null);
564    },
565
566    _requestId: function(request)
567    {
568        if (!request._extensionRequestId) {
569            request._extensionRequestId = ++this._lastRequestId;
570            this._requests[request._extensionRequestId] = request;
571        }
572        return request._extensionRequestId;
573    },
574
575    _requestById: function(id)
576    {
577        return this._requests[id];
578    },
579
580    _onAddAuditCategory: function(message, port)
581    {
582        var category = new WebInspector.ExtensionAuditCategory(port._extensionOrigin, message.id, message.displayName, message.resultCount);
583        if (WebInspector.inspectorView.panel("audits").getCategory(category.id))
584            return this._status.E_EXISTS(category.id);
585        this._clientObjects[message.id] = category;
586        // FIXME: register module manager extension instead of waking up audits module.
587        WebInspector.inspectorView.panel("audits").addCategory(category);
588    },
589
590    _onAddAuditResult: function(message)
591    {
592        var auditResult = this._clientObjects[message.resultId];
593        if (!auditResult)
594            return this._status.E_NOTFOUND(message.resultId);
595        try {
596            auditResult.addResult(message.displayName, message.description, message.severity, message.details);
597        } catch (e) {
598            return e;
599        }
600        return this._status.OK();
601    },
602
603    _onUpdateAuditProgress: function(message)
604    {
605        var auditResult = this._clientObjects[message.resultId];
606        if (!auditResult)
607            return this._status.E_NOTFOUND(message.resultId);
608        auditResult.updateProgress(Math.min(Math.max(0, message.progress), 1));
609    },
610
611    _onStopAuditCategoryRun: function(message)
612    {
613        var auditRun = this._clientObjects[message.resultId];
614        if (!auditRun)
615            return this._status.E_NOTFOUND(message.resultId);
616        auditRun.done();
617    },
618
619    _onForwardKeyboardEvent: function(message)
620    {
621        const Esc = "U+001B";
622        message.entries.forEach(handleEventEntry);
623
624        function handleEventEntry(entry)
625        {
626            if (!entry.ctrlKey && !entry.altKey && !entry.metaKey && !/^F\d+$/.test(entry.keyIdentifier) && entry.keyIdentifier !== Esc)
627                return;
628            // Fool around closure compiler -- it has its own notion of both KeyboardEvent constructor
629            // and initKeyboardEvent methods and overriding these in externs.js does not have effect.
630            var event = new window.KeyboardEvent(entry.eventType, {
631                keyIdentifier: entry.keyIdentifier,
632                location: entry.location,
633                ctrlKey: entry.ctrlKey,
634                altKey: entry.altKey,
635                shiftKey: entry.shiftKey,
636                metaKey: entry.metaKey
637            });
638            event.__keyCode = keyCodeForEntry(entry);
639            document.dispatchEvent(event);
640        }
641
642        function keyCodeForEntry(entry)
643        {
644            var keyCode = entry.keyCode;
645            if (!keyCode) {
646                // This is required only for synthetic events (e.g. dispatched in tests).
647                var match = entry.keyIdentifier.match(/^U\+([\dA-Fa-f]+)$/);
648                if (match)
649                    keyCode = parseInt(match[1], 16);
650            }
651            return keyCode || 0;
652        }
653    },
654
655    _dispatchCallback: function(requestId, port, result)
656    {
657        if (requestId)
658            port.postMessage({ command: "callback", requestId: requestId, result: result });
659    },
660
661    _initExtensions: function()
662    {
663        this._registerAutosubscriptionTargetManagerHandler(WebInspector.extensionAPI.Events.ConsoleMessageAdded,
664            WebInspector.ConsoleModel, WebInspector.ConsoleModel.Events.MessageAdded, this._notifyConsoleMessageAdded);
665        this._registerAutosubscriptionHandler(WebInspector.extensionAPI.Events.ResourceAdded,
666            WebInspector.workspace, WebInspector.Workspace.Events.UISourceCodeAdded, this._notifyResourceAdded);
667        this._registerAutosubscriptionTargetManagerHandler(WebInspector.extensionAPI.Events.NetworkRequestFinished,
668            WebInspector.NetworkManager, WebInspector.NetworkManager.EventTypes.RequestFinished, this._notifyRequestFinished);
669
670        /**
671         * @this {WebInspector.ExtensionServer}
672         */
673        function onElementsSubscriptionStarted()
674        {
675            WebInspector.notifications.addEventListener(WebInspector.NotificationService.Events.SelectedNodeChanged, this._notifyElementsSelectionChanged, this);
676        }
677
678        /**
679         * @this {WebInspector.ExtensionServer}
680         */
681        function onElementsSubscriptionStopped()
682        {
683            WebInspector.notifications.removeEventListener(WebInspector.NotificationService.Events.SelectedNodeChanged, this._notifyElementsSelectionChanged, this);
684        }
685
686        this._registerSubscriptionHandler(WebInspector.extensionAPI.Events.PanelObjectSelected + "elements",
687            onElementsSubscriptionStarted.bind(this), onElementsSubscriptionStopped.bind(this));
688
689        this._registerAutosubscriptionHandler(WebInspector.extensionAPI.Events.PanelObjectSelected + "sources",
690            WebInspector.notifications,
691            WebInspector.SourceFrame.Events.SelectionChanged,
692            this._notifySourceFrameSelectionChanged);
693        this._registerResourceContentCommittedHandler(this._notifyUISourceCodeContentCommitted);
694
695        WebInspector.targetManager.addEventListener(WebInspector.TargetManager.Events.InspectedURLChanged,
696            this._inspectedURLChanged, this);
697
698        InspectorExtensionRegistry.getExtensionsAsync();
699    },
700
701    /**
702     * @param {!WebInspector.TextRange} textRange
703     */
704    _makeSourceSelection: function(textRange)
705    {
706        var sourcesPanel = WebInspector.inspectorView.panel("sources");
707        var selection = {
708            startLine: textRange.startLine,
709            startColumn: textRange.startColumn,
710            endLine: textRange.endLine,
711            endColumn: textRange.endColumn,
712            url: sourcesPanel.sourcesView().currentUISourceCode().uri()
713        };
714
715        return selection;
716    },
717
718    _notifySourceFrameSelectionChanged: function(event)
719    {
720        this._postNotification(WebInspector.extensionAPI.Events.PanelObjectSelected + "sources", this._makeSourceSelection(event.data));
721    },
722
723    _notifyConsoleMessageAdded: function(event)
724    {
725        this._postNotification(WebInspector.extensionAPI.Events.ConsoleMessageAdded, this._makeConsoleMessage(event.data));
726    },
727
728    _notifyResourceAdded: function(event)
729    {
730        var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data);
731        this._postNotification(WebInspector.extensionAPI.Events.ResourceAdded, this._makeResource(uiSourceCode));
732    },
733
734    _notifyUISourceCodeContentCommitted: function(event)
735    {
736        var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data.uiSourceCode);
737        var content = /** @type {string} */ (event.data.content);
738        this._postNotification(WebInspector.extensionAPI.Events.ResourceContentCommitted, this._makeResource(uiSourceCode), content);
739    },
740
741    _notifyRequestFinished: function(event)
742    {
743        var request = /** @type {!WebInspector.NetworkRequest} */ (event.data);
744        // Wake up the "network" module for HAR operations.
745        WebInspector.inspectorView.panel("network");
746        this._postNotification(WebInspector.extensionAPI.Events.NetworkRequestFinished, this._requestId(request), (new WebInspector.HAREntry(request)).build());
747    },
748
749    _notifyElementsSelectionChanged: function()
750    {
751        this._postNotification(WebInspector.extensionAPI.Events.PanelObjectSelected + "elements");
752    },
753
754    /**
755     * @param {!Array.<!ExtensionDescriptor>} extensionInfos
756     */
757    addExtensions: function(extensionInfos)
758    {
759        extensionInfos.forEach(this._addExtension, this);
760    },
761
762    /**
763     * @param {!ExtensionDescriptor} extensionInfo
764     */
765    _addExtension: function(extensionInfo)
766    {
767        const urlOriginRegExp = new RegExp("([^:]+:\/\/[^/]*)\/"); // Can't use regexp literal here, MinJS chokes on it.
768        var startPage = extensionInfo.startPage;
769        var name = extensionInfo.name;
770
771        try {
772            var originMatch = urlOriginRegExp.exec(startPage);
773            if (!originMatch) {
774                console.error("Skipping extension with invalid URL: " + startPage);
775                return false;
776            }
777            var extensionOrigin = originMatch[1];
778            if (!this._registeredExtensions[extensionOrigin]) {
779                // See ExtensionAPI.js for details.
780                InspectorFrontendHost.setInjectedScriptForOrigin(extensionOrigin, buildExtensionAPIInjectedScript(extensionInfo));
781                this._registeredExtensions[extensionOrigin] = { name: name };
782            }
783            var iframe = document.createElement("iframe");
784            iframe.src = startPage;
785            iframe.style.display = "none";
786            document.body.appendChild(iframe);
787        } catch (e) {
788            console.error("Failed to initialize extension " + startPage + ":" + e);
789            return false;
790        }
791        return true;
792    },
793
794    _registerExtension: function(origin, port)
795    {
796        if (!this._registeredExtensions.hasOwnProperty(origin)) {
797            if (origin !== window.location.origin) // Just ignore inspector frames.
798                console.error("Ignoring unauthorized client request from " + origin);
799            return;
800        }
801        port._extensionOrigin = origin;
802        port.addEventListener("message", this._onmessage.bind(this), false);
803        port.start();
804    },
805
806    _onWindowMessage: function(event)
807    {
808        if (event.data === "registerExtension")
809            this._registerExtension(event.origin, event.ports[0]);
810    },
811
812    _onmessage: function(event)
813    {
814        var message = event.data;
815        var result;
816
817        if (message.command in this._handlers)
818            result = this._handlers[message.command](message, event.target);
819        else
820            result = this._status.E_NOTSUPPORTED(message.command);
821
822        if (result && message.requestId)
823            this._dispatchCallback(message.requestId, event.target, result);
824    },
825
826    _registerHandler: function(command, callback)
827    {
828        console.assert(command);
829        this._handlers[command] = callback;
830    },
831
832    _registerSubscriptionHandler: function(eventTopic, onSubscribeFirst, onUnsubscribeLast)
833    {
834        this._subscriptionStartHandlers[eventTopic] = onSubscribeFirst;
835        this._subscriptionStopHandlers[eventTopic] = onUnsubscribeLast;
836    },
837
838    /**
839     * @param {string} eventTopic
840     * @param {!Object} eventTarget
841     * @param {string} frontendEventType
842     * @param {function(!WebInspector.Event)} handler
843     */
844    _registerAutosubscriptionHandler: function(eventTopic, eventTarget, frontendEventType, handler)
845    {
846        this._registerSubscriptionHandler(eventTopic,
847            eventTarget.addEventListener.bind(eventTarget, frontendEventType, handler, this),
848            eventTarget.removeEventListener.bind(eventTarget, frontendEventType, handler, this));
849    },
850
851    /**
852     * @param {string} eventTopic
853     * @param {!Function} modelClass
854     * @param {string} frontendEventType
855     * @param {function(!WebInspector.Event)} handler
856     */
857    _registerAutosubscriptionTargetManagerHandler: function(eventTopic, modelClass, frontendEventType, handler)
858    {
859        this._registerSubscriptionHandler(eventTopic,
860            WebInspector.targetManager.addModelListener.bind(WebInspector.targetManager, modelClass, frontendEventType, handler, this),
861            WebInspector.targetManager.removeModelListener.bind(WebInspector.targetManager, modelClass, frontendEventType, handler, this));
862    },
863
864    _registerResourceContentCommittedHandler: function(handler)
865    {
866        /**
867         * @this {WebInspector.ExtensionServer}
868         */
869        function addFirstEventListener()
870        {
871            WebInspector.workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeContentCommitted, handler, this);
872            WebInspector.workspace.setHasResourceContentTrackingExtensions(true);
873        }
874
875        /**
876         * @this {WebInspector.ExtensionServer}
877         */
878        function removeLastEventListener()
879        {
880            WebInspector.workspace.setHasResourceContentTrackingExtensions(false);
881            WebInspector.workspace.removeEventListener(WebInspector.Workspace.Events.UISourceCodeContentCommitted, handler, this);
882        }
883
884        this._registerSubscriptionHandler(WebInspector.extensionAPI.Events.ResourceContentCommitted,
885            addFirstEventListener.bind(this),
886            removeLastEventListener.bind(this));
887    },
888
889    _expandResourcePath: function(extensionPath, resourcePath)
890    {
891        if (!resourcePath)
892            return;
893        return extensionPath + this._normalizePath(resourcePath);
894    },
895
896    _normalizePath: function(path)
897    {
898        var source = path.split("/");
899        var result = [];
900
901        for (var i = 0; i < source.length; ++i) {
902            if (source[i] === ".")
903                continue;
904            // Ignore empty path components resulting from //, as well as a leading and traling slashes.
905            if (source[i] === "")
906                continue;
907            if (source[i] === "..")
908                result.pop();
909            else
910                result.push(source[i]);
911        }
912        return "/" + result.join("/");
913    },
914
915    /**
916     * @param {string} expression
917     * @param {boolean} exposeCommandLineAPI
918     * @param {boolean} returnByValue
919     * @param {?Object} options
920     * @param {string} securityOrigin
921     * @param {function(?string, !RuntimeAgent.RemoteObject, boolean=)} callback
922     * @return {!WebInspector.ExtensionStatus.Record|undefined}
923     */
924    evaluate: function(expression, exposeCommandLineAPI, returnByValue, options, securityOrigin, callback)
925    {
926        var contextId;
927
928        /**
929         * @param {string} url
930         * @return {boolean}
931         */
932        function resolveURLToFrame(url)
933        {
934            var found;
935            function hasMatchingURL(frame)
936            {
937                found = (frame.url === url) ? frame : null;
938                return found;
939            }
940            WebInspector.resourceTreeModel.frames().some(hasMatchingURL);
941            return found;
942        }
943
944        if (typeof options === "object") {
945            var frame = options.frameURL ? resolveURLToFrame(options.frameURL) : WebInspector.resourceTreeModel.mainFrame;
946            if (!frame) {
947                if (options.frameURL)
948                    console.warn("evaluate: there is no frame with URL " + options.frameURL);
949                else
950                    console.warn("evaluate: the main frame is not yet available");
951                return this._status.E_NOTFOUND(options.frameURL || "<top>");
952            }
953
954            var contextSecurityOrigin;
955            if (options.useContentScriptContext)
956                contextSecurityOrigin = securityOrigin;
957            else if (options.scriptExecutionContext)
958                contextSecurityOrigin = options.scriptExecutionContext;
959
960            var context;
961            var executionContexts = WebInspector.runtimeModel.executionContexts();
962            if (contextSecurityOrigin) {
963                for (var i = 0; i < executionContexts.length; ++i) {
964                    var executionContext = executionContexts[i];
965                    if (executionContext.frameId === frame.id && executionContext.origin === contextSecurityOrigin && !executionContext.isMainWorldContext)
966                        context = executionContext;
967
968                }
969                if (!context) {
970                    console.warn("The JavaScript context " + contextSecurityOrigin + " was not found in the frame " + frame.url)
971                    return this._status.E_NOTFOUND(contextSecurityOrigin)
972                }
973            } else {
974                for (var i = 0; i < executionContexts.length; ++i) {
975                    var executionContext = executionContexts[i];
976                    if (executionContext.frameId === frame.id && executionContext.isMainWorldContext)
977                        context = executionContext;
978
979                }
980                if (!context)
981                    return this._status.E_FAILED(frame.url + " has no execution context");
982            }
983
984            contextId = context.id;
985        }
986        RuntimeAgent.evaluate(expression, "extension", exposeCommandLineAPI, true, contextId, returnByValue, false, callback);
987    }
988}
989
990/**
991 * @constructor
992 * @param {string} name
993 * @param {string} title
994 * @param {!WebInspector.Panel} panel
995 * @implements {WebInspector.PanelDescriptor}
996 */
997WebInspector.ExtensionServerPanelDescriptor = function(name, title, panel)
998{
999    this._name = name;
1000    this._title = title;
1001    this._panel = panel;
1002}
1003
1004WebInspector.ExtensionServerPanelDescriptor.prototype = {
1005    /**
1006     * @return {string}
1007     */
1008    name: function()
1009    {
1010        return this._name;
1011    },
1012
1013    /**
1014     * @return {string}
1015     */
1016    title: function()
1017    {
1018        return this._title;
1019    },
1020
1021    /**
1022     * @return {!WebInspector.Panel}
1023     */
1024    panel: function()
1025    {
1026        return this._panel;
1027    }
1028}
1029
1030/**
1031 * @constructor
1032 */
1033WebInspector.ExtensionStatus = function()
1034{
1035    /**
1036     * @param {string} code
1037     * @param {string} description
1038     * @return {!WebInspector.ExtensionStatus.Record}
1039     */
1040    function makeStatus(code, description)
1041    {
1042        var details = Array.prototype.slice.call(arguments, 2);
1043        var status = { code: code, description: description, details: details };
1044        if (code !== "OK") {
1045            status.isError = true;
1046            console.log("Extension server error: " + String.vsprintf(description, details));
1047        }
1048        return status;
1049    }
1050
1051    this.OK = makeStatus.bind(null, "OK", "OK");
1052    this.E_EXISTS = makeStatus.bind(null, "E_EXISTS", "Object already exists: %s");
1053    this.E_BADARG = makeStatus.bind(null, "E_BADARG", "Invalid argument %s: %s");
1054    this.E_BADARGTYPE = makeStatus.bind(null, "E_BADARGTYPE", "Invalid type for argument %s: got %s, expected %s");
1055    this.E_NOTFOUND = makeStatus.bind(null, "E_NOTFOUND", "Object not found: %s");
1056    this.E_NOTSUPPORTED = makeStatus.bind(null, "E_NOTSUPPORTED", "Object does not support requested operation: %s");
1057    this.E_PROTOCOLERROR = makeStatus.bind(null, "E_PROTOCOLERROR", "Inspector protocol error: %s");
1058    this.E_FAILED = makeStatus.bind(null, "E_FAILED", "Operation failed: %s");
1059}
1060
1061/**
1062 * @typedef {{code: string, description: string, details: !Array.<*>}}
1063 */
1064WebInspector.ExtensionStatus.Record;
1065
1066WebInspector.extensionAPI = {};
1067defineCommonExtensionSymbols(WebInspector.extensionAPI);
1068