c939e99a02db48d959ba1d3e729281dd7b8e6e76
[WebKit-https.git] / Source / WebCore / inspector / front-end / ExtensionServer.js
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  */
34 WebInspector.ExtensionServer = function()
35 {
36     this._clientObjects = {};
37     this._handlers = {};
38     this._subscribers = {};
39     this._subscriptionStartHandlers = {};
40     this._subscriptionStopHandlers = {};
41     this._extraHeaders = {};
42     this._requests = {};
43     this._lastRequestId = 0;
44     this._registeredExtensions = {};
45     this._status = new WebInspector.ExtensionStatus();
46
47     var commands = WebInspector.extensionAPI.Commands;
48
49     this._registerHandler(commands.AddAuditCategory, this._onAddAuditCategory.bind(this));
50     this._registerHandler(commands.AddAuditResult, this._onAddAuditResult.bind(this));
51     this._registerHandler(commands.AddConsoleMessage, this._onAddConsoleMessage.bind(this));
52     this._registerHandler(commands.AddRequestHeaders, this._onAddRequestHeaders.bind(this));
53     this._registerHandler(commands.CreatePanel, this._onCreatePanel.bind(this));
54     this._registerHandler(commands.CreateSidebarPane, this._onCreateSidebarPane.bind(this));
55     this._registerHandler(commands.CreateStatusBarButton, this._onCreateStatusBarButton.bind(this));
56     this._registerHandler(commands.EvaluateOnInspectedPage, this._onEvaluateOnInspectedPage.bind(this));
57     this._registerHandler(commands.GetHAR, this._onGetHAR.bind(this));
58     this._registerHandler(commands.GetConsoleMessages, this._onGetConsoleMessages.bind(this));
59     this._registerHandler(commands.GetPageResources, this._onGetPageResources.bind(this));
60     this._registerHandler(commands.GetRequestContent, this._onGetRequestContent.bind(this));
61     this._registerHandler(commands.GetResourceContent, this._onGetResourceContent.bind(this));
62     this._registerHandler(commands.Log, this._onLog.bind(this));
63     this._registerHandler(commands.Reload, this._onReload.bind(this));
64     this._registerHandler(commands.SetOpenResourceHandler, this._onSetOpenResourceHandler.bind(this));
65     this._registerHandler(commands.SetResourceContent, this._onSetResourceContent.bind(this));
66     this._registerHandler(commands.SetSidebarHeight, this._onSetSidebarHeight.bind(this));
67     this._registerHandler(commands.SetSidebarContent, this._onSetSidebarContent.bind(this));
68     this._registerHandler(commands.SetSidebarPage, this._onSetSidebarPage.bind(this));
69     this._registerHandler(commands.ShowPanel, this._onShowPanel.bind(this));
70     this._registerHandler(commands.StopAuditCategoryRun, this._onStopAuditCategoryRun.bind(this));
71     this._registerHandler(commands.Subscribe, this._onSubscribe.bind(this));
72     this._registerHandler(commands.Unsubscribe, this._onUnsubscribe.bind(this));
73     this._registerHandler(commands.UpdateButton, this._onUpdateButton.bind(this));
74     this._registerHandler(commands.UpdateAuditProgress, this._onUpdateAuditProgress.bind(this));
75
76     window.addEventListener("message", this._onWindowMessage.bind(this), false);
77 }
78
79 WebInspector.ExtensionServer.prototype = {
80     hasExtensions: function()
81     {
82         return !!Object.keys(this._registeredExtensions).length;
83     },
84
85     notifySearchAction: function(panelId, action, searchString)
86     {
87         this._postNotification(WebInspector.extensionAPI.Events.PanelSearch + panelId, action, searchString);
88     },
89
90     notifyViewShown: function(identifier, frameIndex)
91     {
92         this._postNotification(WebInspector.extensionAPI.Events.ViewShown + identifier, frameIndex);
93     },
94
95     notifyViewHidden: function(identifier)
96     {
97         this._postNotification(WebInspector.extensionAPI.Events.ViewHidden + identifier);
98     },
99
100     notifyButtonClicked: function(identifier)
101     {
102         this._postNotification(WebInspector.extensionAPI.Events.ButtonClicked + identifier);
103     },
104
105     _inspectedURLChanged: function(event)
106     {
107         this._requests = {};
108         var url = event.data;
109         this._postNotification(WebInspector.extensionAPI.Events.InspectedURLChanged, url);
110     },
111
112     startAuditRun: function(category, auditRun)
113     {
114         this._clientObjects[auditRun.id] = auditRun;
115         this._postNotification("audit-started-" + category.id, auditRun.id);
116     },
117
118     stopAuditRun: function(auditRun)
119     {
120         delete this._clientObjects[auditRun.id];
121     },
122
123     /**
124      * @param {...*} vararg
125      */
126     _postNotification: function(type, vararg)
127     {
128         var subscribers = this._subscribers[type];
129         if (!subscribers)
130             return;
131         var message = {
132             command: "notify-" + type,
133             arguments: Array.prototype.slice.call(arguments, 1)
134         };
135         for (var i = 0; i < subscribers.length; ++i)
136             subscribers[i].postMessage(message);
137     },
138
139     _onSubscribe: function(message, port)
140     {
141         var subscribers = this._subscribers[message.type];
142         if (subscribers)
143             subscribers.push(port);
144         else {
145             this._subscribers[message.type] = [ port ];
146             if (this._subscriptionStartHandlers[message.type])
147                 this._subscriptionStartHandlers[message.type]();
148         }
149     },
150
151     _onUnsubscribe: function(message, port)
152     {
153         var subscribers = this._subscribers[message.type];
154         if (!subscribers)
155             return;
156         subscribers.remove(port);
157         if (!subscribers.length) {
158             delete this._subscribers[message.type];
159             if (this._subscriptionStopHandlers[message.type])
160                 this._subscriptionStopHandlers[message.type]();
161         }
162     },
163
164     _onAddRequestHeaders: function(message)
165     {
166         var id = message.extensionId;
167         if (typeof id !== "string")
168             return this._status.E_BADARGTYPE("extensionId", typeof id, "string");
169         var extensionHeaders = this._extraHeaders[id];
170         if (!extensionHeaders) {
171             extensionHeaders = {};
172             this._extraHeaders[id] = extensionHeaders;
173         }
174         for (var name in message.headers)
175             extensionHeaders[name] = message.headers[name];
176         var allHeaders = /** @type NetworkAgent.Headers */ ({});
177         for (var extension in this._extraHeaders) {
178             var headers = this._extraHeaders[extension];
179             for (name in headers) {
180                 if (typeof headers[name] === "string")
181                     allHeaders[name] = headers[name];
182             }
183         }
184         NetworkAgent.setExtraHTTPHeaders(allHeaders);
185     },
186
187     _onCreatePanel: function(message, port)
188     {
189         var id = message.id;
190         // The ids are generated on the client API side and must be unique, so the check below
191         // shouldn't be hit unless someone is bypassing the API.
192         if (id in this._clientObjects || id in WebInspector.panels)
193             return this._status.E_EXISTS(id);
194
195         var page = this._expandResourcePath(port._extensionOrigin, message.page);
196         var panelDescriptor = new WebInspector.PanelDescriptor(id, message.title, undefined, undefined, new WebInspector.ExtensionPanel(id, page));
197         panelDescriptor.setIconURL(this._expandResourcePath(port._extensionOrigin, message.icon));
198         this._clientObjects[id] = panelDescriptor.panel();
199         WebInspector.inspectorView.addPanel(panelDescriptor);
200         return this._status.OK();
201     },
202
203     _onShowPanel: function(message)
204     {
205         // Note: WebInspector.showPanel already sanitizes input.
206         WebInspector.showPanel(message.id);
207     },
208
209     _onCreateStatusBarButton: function(message, port)
210     {
211         var panel = this._clientObjects[message.panel];
212         if (!panel || !(panel instanceof WebInspector.ExtensionPanel))
213             return this._status.E_NOTFOUND(message.panel);
214         var button = new WebInspector.ExtensionButton(message.id, this._expandResourcePath(port._extensionOrigin, message.icon), message.tooltip, message.disabled);
215         this._clientObjects[message.id] = button;
216         panel.addStatusBarItem(button.element);
217         return this._status.OK();
218     },
219
220     _onUpdateButton: function(message, port)
221     {
222         var button = this._clientObjects[message.id];
223         if (!button || !(button instanceof WebInspector.ExtensionButton))
224             return this._status.E_NOTFOUND(message.id);
225         button.update(this._expandResourcePath(port._extensionOrigin, message.icon), message.tooltip, message.disabled);
226         return this._status.OK();
227     },
228
229     _onCreateSidebarPane: function(message)
230     {
231         var panel = WebInspector.panel(message.panel);
232         if (!panel)
233             return this._status.E_NOTFOUND(message.panel);
234         if (!panel.sidebarPaneView || !panel.sidebarPanes)
235             return this._status.E_NOTSUPPORTED();
236         var id = message.id;
237         var sidebar = new WebInspector.ExtensionSidebarPane(message.title, message.id);
238         this._clientObjects[id] = sidebar;
239         panel.sidebarPanes[id] = sidebar;
240         panel.sidebarPaneView.addPane(sidebar);
241
242         return this._status.OK();
243     },
244
245     _onSetSidebarHeight: function(message)
246     {
247         var sidebar = this._clientObjects[message.id];
248         if (!sidebar)
249             return this._status.E_NOTFOUND(message.id);
250         sidebar.setHeight(message.height);
251         return this._status.OK();
252     },
253
254     _onSetSidebarContent: function(message, port)
255     {
256         var sidebar = this._clientObjects[message.id];
257         if (!sidebar)
258             return this._status.E_NOTFOUND(message.id);
259         function callback(error)
260         {
261             var result = error ? this._status.E_FAILED(error) : this._status.OK();
262             this._dispatchCallback(message.requestId, port, result);
263         }
264         if (message.evaluateOnPage)
265             return sidebar.setExpression(message.expression, message.rootTitle, message.evaluateOptions, port._extensionOrigin, callback.bind(this));
266         sidebar.setObject(message.expression, message.rootTitle, callback.bind(this));
267     },
268
269     _onSetSidebarPage: function(message, port)
270     {
271         var sidebar = this._clientObjects[message.id];
272         if (!sidebar)
273             return this._status.E_NOTFOUND(message.id);
274         sidebar.setPage(this._expandResourcePath(port._extensionOrigin, message.page));
275     },
276
277     _onSetOpenResourceHandler: function(message, port)
278     {
279         var name = this._registeredExtensions[port._extensionOrigin].name || ("Extension " + port._extensionOrigin);
280         if (message.handlerPresent)
281             WebInspector.openAnchorLocationRegistry.registerHandler(name, this._handleOpenURL.bind(this, port));
282         else
283             WebInspector.openAnchorLocationRegistry.unregisterHandler(name);
284     },
285
286     _handleOpenURL: function(port, details)
287     {
288         var url = /** @type {string} */ (details.url);
289         var contentProvider = WebInspector.workspace.uiSourceCodeForOriginURL(url) || WebInspector.resourceForURL(url);
290         if (!contentProvider)
291             return false;
292             
293         var lineNumber = details.lineNumber;
294         if (typeof lineNumber === "number")
295             lineNumber += 1;
296         port.postMessage({
297             command: "open-resource",
298             resource: this._makeResource(contentProvider),
299             lineNumber: lineNumber
300         });
301         return true;
302     },
303
304     _onLog: function(message)
305     {
306         WebInspector.log(message.message);
307     },
308
309     _onReload: function(message)
310     {
311         var options = /** @type ExtensionReloadOptions */ (message.options || {});
312         NetworkAgent.setUserAgentOverride(typeof options.userAgent === "string" ? options.userAgent : "");
313         var injectedScript;
314         if (options.injectedScript) {
315             // Wrap client script into anonymous function, return another anonymous function that
316             // returns empty object for compatibility with InjectedScriptManager on the backend.
317             injectedScript = "((function(){" + options.injectedScript + "})(),function(){return {}})";
318         }
319         PageAgent.reload(!!options.ignoreCache, injectedScript);
320         return this._status.OK();
321     },
322
323     _onEvaluateOnInspectedPage: function(message, port)
324     {
325         /**
326          * @param {?Protocol.Error} error
327          * @param {RuntimeAgent.RemoteObject} resultPayload
328          * @param {boolean=} wasThrown
329          */
330         function callback(error, resultPayload, wasThrown)
331         {
332             var result = {};
333             if (error) {
334                 result.isException = true;
335                 result.value = error.toString();
336             }  else if (wasThrown) {
337                 result.isException = true;
338                 result.value = resultPayload.description;
339             } else {
340                 result.value = resultPayload.value;
341             }
342       
343             this._dispatchCallback(message.requestId, port, result);
344         }
345         return this.evaluate(message.expression, true, true, message.evaluateOptions, port._extensionOrigin, callback.bind(this));
346     },
347
348     _onGetConsoleMessages: function()
349     {
350         return WebInspector.console.messages.map(this._makeConsoleMessage);
351     },
352
353     _onAddConsoleMessage: function(message)
354     {
355         function convertSeverity(level)
356         {
357             switch (level) {
358                 case WebInspector.extensionAPI.console.Severity.Tip:
359                     return WebInspector.ConsoleMessage.MessageLevel.Tip;
360                 case WebInspector.extensionAPI.console.Severity.Log:
361                     return WebInspector.ConsoleMessage.MessageLevel.Log;
362                 case WebInspector.extensionAPI.console.Severity.Warning:
363                     return WebInspector.ConsoleMessage.MessageLevel.Warning;
364                 case WebInspector.extensionAPI.console.Severity.Error:
365                     return WebInspector.ConsoleMessage.MessageLevel.Error;
366                 case WebInspector.extensionAPI.console.Severity.Debug:
367                     return WebInspector.ConsoleMessage.MessageLevel.Debug;
368             }
369         }
370         var level = convertSeverity(message.severity);
371         if (!level)
372             return this._status.E_BADARG("message.severity", message.severity);
373
374         var consoleMessage = WebInspector.ConsoleMessage.create(
375             WebInspector.ConsoleMessage.MessageSource.JS,
376             level,
377             message.text,
378             WebInspector.ConsoleMessage.MessageType.Log,
379             message.url,
380             message.line);
381         WebInspector.console.addMessage(consoleMessage);
382     },
383
384     _makeConsoleMessage: function(message)
385     {
386         function convertLevel(level)
387         {
388             if (!level)
389                 return;
390             switch (level) {
391                 case WebInspector.ConsoleMessage.MessageLevel.Tip:
392                     return WebInspector.extensionAPI.console.Severity.Tip;
393                 case WebInspector.ConsoleMessage.MessageLevel.Log:
394                     return WebInspector.extensionAPI.console.Severity.Log;
395                 case WebInspector.ConsoleMessage.MessageLevel.Warning:
396                     return WebInspector.extensionAPI.console.Severity.Warning;
397                 case WebInspector.ConsoleMessage.MessageLevel.Error:
398                     return WebInspector.extensionAPI.console.Severity.Error;
399                 case WebInspector.ConsoleMessage.MessageLevel.Debug:
400                     return WebInspector.extensionAPI.console.Severity.Debug;
401                 default:
402                     return WebInspector.extensionAPI.console.Severity.Log;
403             }
404         }
405         var result = {
406             severity: convertLevel(message.level),
407             text: message.text,
408         };
409         if (message.url)
410             result.url = message.url;
411         if (message.line)
412             result.line = message.line;
413         return result;
414     },
415
416     _onGetHAR: function()
417     {
418         var requests = WebInspector.networkLog.requests;
419         var harLog = (new WebInspector.HARLog(requests)).build();
420         for (var i = 0; i < harLog.entries.length; ++i)
421             harLog.entries[i]._requestId = this._requestId(requests[i]);
422         return harLog;
423     },
424
425     /**
426      * @param {WebInspector.ContentProvider} contentProvider
427      */
428     _makeResource: function(contentProvider)
429     {
430         return {
431             url: contentProvider.contentURL(),
432             type: contentProvider.contentType().name()
433         };
434     },
435
436     _onGetPageResources: function()
437     {
438         var resources = {};
439
440         function pushResourceData(contentProvider)
441         {
442             if (!resources[contentProvider.contentURL()])
443                 resources[contentProvider.contentURL()] = this._makeResource(contentProvider);
444         }
445         var uiSourceCodes = WebInspector.workspace.project(WebInspector.projectTypes.Network).uiSourceCodes();
446         uiSourceCodes.forEach(pushResourceData.bind(this));
447         WebInspector.resourceTreeModel.forAllResources(pushResourceData.bind(this));
448         return Object.values(resources);
449     },
450
451     /**
452      * @param {WebInspector.ContentProvider} contentProvider
453      */
454     _getResourceContent: function(contentProvider, message, port)
455     {
456         /**
457          * @param {?string} content
458          * @param {boolean} contentEncoded
459          * @param {string} mimeType
460          */
461         function onContentAvailable(content, contentEncoded, mimeType)
462         {
463             var response = {
464                 encoding: contentEncoded ? "base64" : "",
465                 content: content
466             };
467             this._dispatchCallback(message.requestId, port, response);
468         }
469         contentProvider.requestContent(onContentAvailable.bind(this));
470     },
471
472     _onGetRequestContent: function(message, port)
473     {
474         var request = this._requestById(message.id);
475         if (!request)
476             return this._status.E_NOTFOUND(message.id);
477         this._getResourceContent(request, message, port);
478     },
479
480     _onGetResourceContent: function(message, port)
481     {
482         var url = /** @type {string} */ (message.url);
483         var contentProvider = WebInspector.workspace.uiSourceCodeForOriginURL(url) || WebInspector.resourceForURL(url);
484         if (!contentProvider)
485             return this._status.E_NOTFOUND(url);
486         this._getResourceContent(contentProvider, message, port);
487     },
488
489     _onSetResourceContent: function(message, port)
490     {
491         /**
492          * @param {?Protocol.Error} error
493          */
494         function callbackWrapper(error)
495         {
496             var response = error ? this._status.E_FAILED(error) : this._status.OK();
497             this._dispatchCallback(message.requestId, port, response);
498         }
499
500         var url = /** @type {string} */ (message.url);
501         var uiSourceCode = WebInspector.workspace.uiSourceCodeForOriginURL(url);
502         if (!uiSourceCode) {
503             var resource = WebInspector.resourceTreeModel.resourceForURL(url);
504             if (!resource)
505                 return this._status.E_NOTFOUND(url);
506             return this._status.E_NOTSUPPORTED("Resource is not editable")
507         }
508         uiSourceCode.setWorkingCopy(message.content);
509         if (message.commit)
510             uiSourceCode.commitWorkingCopy(callbackWrapper.bind(this));
511         else
512             callbackWrapper.call(this, null);
513     },
514
515     _requestId: function(request)
516     {
517         if (!request._extensionRequestId) {
518             request._extensionRequestId = ++this._lastRequestId;
519             this._requests[request._extensionRequestId] = request;
520         }
521         return request._extensionRequestId;
522     },
523
524     _requestById: function(id)
525     {
526         return this._requests[id];
527     },
528
529     _onAddAuditCategory: function(message, port)
530     {
531         var category = new WebInspector.ExtensionAuditCategory(port._extensionOrigin, message.id, message.displayName, message.resultCount);
532         if (WebInspector.panel("audits").getCategory(category.id))
533             return this._status.E_EXISTS(category.id);
534         this._clientObjects[message.id] = category;
535         WebInspector.panel("audits").addCategory(category);
536     },
537
538     _onAddAuditResult: function(message)
539     {
540         var auditResult = this._clientObjects[message.resultId];
541         if (!auditResult)
542             return this._status.E_NOTFOUND(message.resultId);
543         try {
544             auditResult.addResult(message.displayName, message.description, message.severity, message.details);
545         } catch (e) {
546             return e;
547         }
548         return this._status.OK();
549     },
550
551     _onUpdateAuditProgress: function(message)
552     {
553         var auditResult = this._clientObjects[message.resultId];
554         if (!auditResult)
555             return this._status.E_NOTFOUND(message.resultId);
556         auditResult.updateProgress(Math.min(Math.max(0, message.progress), 1));
557     },
558
559     _onStopAuditCategoryRun: function(message)
560     {
561         var auditRun = this._clientObjects[message.resultId];
562         if (!auditRun)
563             return this._status.E_NOTFOUND(message.resultId);
564         auditRun.done();
565     },
566
567     _dispatchCallback: function(requestId, port, result)
568     {
569         if (requestId)
570             port.postMessage({ command: "callback", requestId: requestId, result: result });
571     },
572
573     initExtensions: function()
574     {
575         this._registerAutosubscriptionHandler(WebInspector.extensionAPI.Events.ConsoleMessageAdded,
576             WebInspector.console, WebInspector.ConsoleModel.Events.MessageAdded, this._notifyConsoleMessageAdded);
577         this._registerAutosubscriptionHandler(WebInspector.extensionAPI.Events.NetworkRequestFinished,
578             WebInspector.networkManager, WebInspector.NetworkManager.EventTypes.RequestFinished, this._notifyRequestFinished);
579         this._registerAutosubscriptionHandler(WebInspector.extensionAPI.Events.ResourceAdded,
580             WebInspector.workspace,
581             WebInspector.UISourceCodeProvider.Events.UISourceCodeAdded,
582             this._notifyResourceAdded);
583         this._registerAutosubscriptionHandler(WebInspector.extensionAPI.Events.ElementsPanelObjectSelected,
584             WebInspector.notifications,
585             WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged,
586             this._notifyElementsSelectionChanged);
587         this._registerAutosubscriptionHandler(WebInspector.extensionAPI.Events.ResourceContentCommitted,
588             WebInspector.workspace,
589             WebInspector.Workspace.Events.UISourceCodeContentCommitted,
590             this._notifyUISourceCodeContentCommitted);
591
592         function onTimelineSubscriptionStarted()
593         {
594             WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.EventTypes.TimelineEventRecorded,
595                 this._notifyTimelineEventRecorded, this);
596             WebInspector.timelineManager.start();
597         }
598         function onTimelineSubscriptionStopped()
599         {
600             WebInspector.timelineManager.stop();
601             WebInspector.timelineManager.removeEventListener(WebInspector.TimelineManager.EventTypes.TimelineEventRecorded,
602                 this._notifyTimelineEventRecorded, this);
603         }
604         this._registerSubscriptionHandler(WebInspector.extensionAPI.Events.TimelineEventRecorded,
605             onTimelineSubscriptionStarted.bind(this), onTimelineSubscriptionStopped.bind(this));
606
607         WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.InspectedURLChanged,
608             this._inspectedURLChanged, this);
609         this._initDone = true;
610         if (this._pendingExtensions) {
611             this._pendingExtensions.forEach(this._innerAddExtension, this);
612             delete this._pendingExtensions;
613         }
614         InspectorExtensionRegistry.getExtensionsAsync();
615     },
616
617     _notifyConsoleMessageAdded: function(event)
618     {
619         this._postNotification(WebInspector.extensionAPI.Events.ConsoleMessageAdded, this._makeConsoleMessage(event.data));
620     },
621
622     _notifyResourceAdded: function(event)
623     {
624         var uiSourceCode = /** @type {WebInspector.UISourceCode} */ (event.data);
625         this._postNotification(WebInspector.extensionAPI.Events.ResourceAdded, this._makeResource(uiSourceCode));
626     },
627
628     _notifyUISourceCodeContentCommitted: function(event)
629     {
630         var uiSourceCode = /** @type {WebInspector.UISourceCode} */ (event.data.uiSourceCode);
631         var content = /** @type {string} */ (event.data.content);
632         this._postNotification(WebInspector.extensionAPI.Events.ResourceContentCommitted, this._makeResource(uiSourceCode), content);
633     },
634
635     _notifyRequestFinished: function(event)
636     {
637         var request = /** @type {WebInspector.NetworkRequest} */ (event.data);
638         this._postNotification(WebInspector.extensionAPI.Events.NetworkRequestFinished, this._requestId(request), (new WebInspector.HAREntry(request)).build());
639     },
640
641     _notifyElementsSelectionChanged: function()
642     {
643         this._postNotification(WebInspector.extensionAPI.Events.ElementsPanelObjectSelected);
644     },
645
646     _notifyTimelineEventRecorded: function(event)
647     {
648         this._postNotification(WebInspector.extensionAPI.Events.TimelineEventRecorded, event.data);
649     },
650
651     /**
652      * @param {Array.<ExtensionDescriptor>} extensions
653      */
654     _addExtensions: function(extensions)
655     {
656         extensions.forEach(this._addExtension, this);
657     },
658
659     _addExtension: function(extensionInfo)
660     {
661         if (this._initDone) {
662             this._innerAddExtension(extensionInfo);
663             return;
664         }
665         if (this._pendingExtensions)
666             this._pendingExtensions.push(extensionInfo);
667         else
668             this._pendingExtensions = [extensionInfo];
669     },
670
671     _innerAddExtension: function(extensionInfo)
672     {
673         const urlOriginRegExp = new RegExp("([^:]+:\/\/[^/]*)\/"); // Can't use regexp literal here, MinJS chokes on it.
674         var startPage = extensionInfo.startPage;
675         var name = extensionInfo.name;
676
677         try {
678             var originMatch = urlOriginRegExp.exec(startPage);
679             if (!originMatch) {
680                 console.error("Skipping extension with invalid URL: " + startPage);
681                 return false;
682             }
683             var extensionOrigin = originMatch[1];
684             if (!this._registeredExtensions[extensionOrigin]) {
685                 // See ExtensionAPI.js and ExtensionCommon.js for details.
686                 InspectorFrontendHost.setInjectedScriptForOrigin(extensionOrigin, buildExtensionAPIInjectedScript(extensionInfo));
687                 this._registeredExtensions[extensionOrigin] = { name: name };
688             }
689             var iframe = document.createElement("iframe");
690             iframe.src = startPage;
691             iframe.style.display = "none";
692             document.body.appendChild(iframe);
693         } catch (e) {
694             console.error("Failed to initialize extension " + startPage + ":" + e);
695             return false;
696         }
697         return true;
698     },
699
700     _onWindowMessage: function(event)
701     {
702         if (event.data === "registerExtension")
703             this._registerExtension(event.origin, event.ports[0]);
704     },
705
706     _registerExtension: function(origin, port)
707     {
708         if (!this._registeredExtensions.hasOwnProperty(origin)) {
709             if (origin !== window.location.origin) // Just ignore inspector frames.
710                 console.error("Ignoring unauthorized client request from " + origin);
711             return;
712         }
713         port._extensionOrigin = origin;
714         port.addEventListener("message", this._onmessage.bind(this), false);
715         port.start();
716     },
717
718     _onmessage: function(event)
719     {
720         var message = event.data;
721         var result;
722
723         if (message.command in this._handlers)
724             result = this._handlers[message.command](message, event.target);
725         else
726             result = this._status.E_NOTSUPPORTED(message.command);
727
728         if (result && message.requestId)
729             this._dispatchCallback(message.requestId, event.target, result);
730     },
731
732     _registerHandler: function(command, callback)
733     {
734         this._handlers[command] = callback;
735     },
736
737     _registerSubscriptionHandler: function(eventTopic, onSubscribeFirst, onUnsubscribeLast)
738     {
739         this._subscriptionStartHandlers[eventTopic] =  onSubscribeFirst;
740         this._subscriptionStopHandlers[eventTopic] =  onUnsubscribeLast;
741     },
742
743     _registerAutosubscriptionHandler: function(eventTopic, eventTarget, frontendEventType, handler)
744     {
745         this._registerSubscriptionHandler(eventTopic,
746             eventTarget.addEventListener.bind(eventTarget, frontendEventType, handler, this),
747             eventTarget.removeEventListener.bind(eventTarget, frontendEventType, handler, this));
748     },
749
750     _expandResourcePath: function(extensionPath, resourcePath)
751     {
752         if (!resourcePath)
753             return;
754         return extensionPath + this._normalizePath(resourcePath);
755     },
756
757     _normalizePath: function(path)
758     {
759         var source = path.split("/");
760         var result = [];
761
762         for (var i = 0; i < source.length; ++i) {
763             if (source[i] === ".")
764                 continue;
765             // Ignore empty path components resulting from //, as well as a leading and traling slashes.
766             if (source[i] === "")
767                 continue;
768             if (source[i] === "..")
769                 result.pop();
770             else
771                 result.push(source[i]);
772         }
773         return "/" + result.join("/");
774     },
775
776     /**
777      * @param {string} expression
778      * @param {boolean} exposeCommandLineAPI
779      * @param {boolean} returnByValue
780      * @param {Object} options
781      * @param {string} securityOrigin
782      * @param {function(?string, ?RuntimeAgent.RemoteObject, boolean=)} callback
783      */
784     evaluate: function(expression, exposeCommandLineAPI, returnByValue, options, securityOrigin, callback) 
785     {
786         var contextId;
787         if (typeof options === "object" && options["useContentScriptContext"]) {
788             var mainFrame = WebInspector.resourceTreeModel.mainFrame;
789             if (!mainFrame)
790                 return this._status.E_FAILED("main frame not available yet");
791             var context = WebInspector.runtimeModel.contextByFrameAndSecurityOrigin(mainFrame, securityOrigin);
792             if (!context)
793                 return this._status.E_NOTFOUND(securityOrigin);
794             contextId = context.id;
795         }
796         RuntimeAgent.evaluate(expression, "extension", exposeCommandLineAPI, true, contextId, returnByValue, false, callback);
797     }
798 }
799
800 /**
801  * @constructor
802  */
803 WebInspector.ExtensionStatus = function()
804 {
805     function makeStatus(code, description)
806     {
807         var details = Array.prototype.slice.call(arguments, 2);
808         var status = { code: code, description: description, details: details };
809         if (code !== "OK") {
810             status.isError = true;
811             console.log("Extension server error: " + String.vsprintf(description, details));
812         }
813         return status;
814     }
815
816     this.OK = makeStatus.bind(null, "OK", "OK");
817     this.E_EXISTS = makeStatus.bind(null, "E_EXISTS", "Object already exists: %s");
818     this.E_BADARG = makeStatus.bind(null, "E_BADARG", "Invalid argument %s: %s");
819     this.E_BADARGTYPE = makeStatus.bind(null, "E_BADARGTYPE", "Invalid type for argument %s: got %s, expected %s");
820     this.E_NOTFOUND = makeStatus.bind(null, "E_NOTFOUND", "Object not found: %s");
821     this.E_NOTSUPPORTED = makeStatus.bind(null, "E_NOTSUPPORTED", "Object does not support requested operation: %s");
822     this.E_FAILED = makeStatus.bind(null, "E_FAILED", "Operation failed: %s");
823 }
824
825 WebInspector.addExtensions = function(extensions)
826 {
827     WebInspector.extensionServer._addExtensions(extensions);
828 }
829
830 WebInspector.extensionAPI = {};
831 defineCommonExtensionSymbols(WebInspector.extensionAPI);
832
833 WebInspector.extensionServer = new WebInspector.ExtensionServer();
834
835 window.addExtension = function(page, name)
836 {
837     WebInspector.extensionServer._addExtension({
838         startPage: page,
839         name: name,
840     });
841 }