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