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
31WebInspector.ExtensionServer = function()
32{
33    this._clientObjects = {};
34    this._handlers = {};
35    this._subscribers = {};
36    this._extraHeaders = {};
37    this._resources = {};
38    this._lastResourceId = 0;
39    this._status = new WebInspector.ExtensionStatus();
40
41    this._registerHandler("addRequestHeaders", this._onAddRequestHeaders.bind(this));
42    this._registerHandler("addAuditCategory", this._onAddAuditCategory.bind(this));
43    this._registerHandler("addAuditResult", this._onAddAuditResult.bind(this));
44    this._registerHandler("createPanel", this._onCreatePanel.bind(this));
45    this._registerHandler("createSidebarPane", this._onCreateSidebarPane.bind(this));
46    this._registerHandler("evaluateOnInspectedPage", this._onEvaluateOnInspectedPage.bind(this));
47    this._registerHandler("getHAR", this._onGetHAR.bind(this));
48    this._registerHandler("getResourceContent", this._onGetResourceContent.bind(this));
49    this._registerHandler("log", this._onLog.bind(this));
50    this._registerHandler("reload", this._onReload.bind(this));
51    this._registerHandler("setSidebarHeight", this._onSetSidebarHeight.bind(this));
52    this._registerHandler("setSidebarContent", this._onSetSidebarContent.bind(this));
53    this._registerHandler("setSidebarPage", this._onSetSidebarPage.bind(this));
54    this._registerHandler("stopAuditCategoryRun", this._onStopAuditCategoryRun.bind(this));
55    this._registerHandler("subscribe", this._onSubscribe.bind(this));
56    this._registerHandler("unsubscribe", this._onUnsubscribe.bind(this));
57
58    window.addEventListener("message", this._onWindowMessage.bind(this), false);
59}
60
61WebInspector.ExtensionServer.prototype = {
62    notifyObjectSelected: function(panelId, objectId)
63    {
64        this._postNotification("panel-objectSelected-" + panelId, objectId);
65    },
66
67    notifySearchAction: function(panelId, action, searchString)
68    {
69        this._postNotification("panel-search-" + panelId, action, searchString);
70    },
71
72    notifyPanelShown: function(panelId)
73    {
74        this._postNotification("panel-shown-" + panelId);
75    },
76
77    notifyPanelHidden: function(panelId)
78    {
79        this._postNotification("panel-hidden-" + panelId);
80    },
81
82    notifyInspectedURLChanged: function(url)
83    {
84        this._postNotification("inspectedURLChanged", url);
85    },
86
87    notifyInspectorReset: function()
88    {
89        this._postNotification("reset");
90    },
91
92    notifyExtensionSidebarUpdated: function(id)
93    {
94        this._postNotification("sidebar-updated-" + id);
95    },
96
97    startAuditRun: function(category, auditRun)
98    {
99        this._clientObjects[auditRun.id] = auditRun;
100        this._postNotification("audit-started-" + category.id, auditRun.id);
101    },
102
103    stopAuditRun: function(auditRun)
104    {
105        delete this._clientObjects[auditRun.id];
106    },
107
108    resetResources: function()
109    {
110        this._resources = {};
111    },
112
113    _notifyResourceFinished: function(event)
114    {
115        var resource = event.data;
116        if (this._hasSubscribers("resource-finished"))
117            this._postNotification("resource-finished", this._resourceId(resource), (new WebInspector.HAREntry(resource)).build());
118    },
119
120    _hasSubscribers: function(type)
121    {
122        return !!this._subscribers[type];
123    },
124
125    _postNotification: function(type, details)
126    {
127        var subscribers = this._subscribers[type];
128        if (!subscribers)
129            return;
130        var message = {
131            command: "notify-" + type,
132            arguments: Array.prototype.slice.call(arguments, 1)
133        };
134        for (var i = 0; i < subscribers.length; ++i)
135            subscribers[i].postMessage(message);
136    },
137
138    _onSubscribe: function(message, port)
139    {
140        var subscribers = this._subscribers[message.type];
141        if (subscribers)
142            subscribers.push(port);
143        else
144            this._subscribers[message.type] = [ port ];
145    },
146
147    _onUnsubscribe: function(message, port)
148    {
149        var subscribers = this._subscribers[message.type];
150        if (!subscribers)
151            return;
152        subscribers.remove(port);
153        if (!subscribers.length)
154            delete this._subscribers[message.type];
155    },
156
157    _onAddRequestHeaders: function(message)
158    {
159        var id = message.extensionId;
160        if (typeof id !== "string")
161            return this._status.E_BADARGTYPE("extensionId", typeof id, "string");
162        var extensionHeaders = this._extraHeaders[id];
163        if (!extensionHeaders) {
164            extensionHeaders = {};
165            this._extraHeaders[id] = extensionHeaders;
166        }
167        for (name in message.headers)
168            extensionHeaders[name] = message.headers[name];
169        var allHeaders = {};
170        for (extension in this._extraHeaders) {
171            var headers = this._extraHeaders[extension];
172            for (name in headers) {
173                if (typeof headers[name] === "string")
174                    allHeaders[name] = headers[name];
175            }
176        }
177        NetworkAgent.setExtraHeaders(allHeaders);
178    },
179
180    _onCreatePanel: function(message, port)
181    {
182        var id = message.id;
183        // The ids are generated on the client API side and must be unique, so the check below
184        // shouldn't be hit unless someone is bypassing the API.
185        if (id in this._clientObjects || id in WebInspector.panels)
186            return this._status.E_EXISTS(id);
187
188        var panel = new WebInspector.ExtensionPanel(id, message.title, message.icon);
189        this._clientObjects[id] = panel;
190        WebInspector.panels[id] = panel;
191        WebInspector.addPanel(panel);
192
193        var iframe = this.createClientIframe(panel.element, message.url);
194        iframe.style.height = "100%";
195        return this._status.OK();
196    },
197
198    _onCreateSidebarPane: function(message, constructor)
199    {
200        var panel = WebInspector.panels[message.panel];
201        if (!panel)
202            return this._status.E_NOTFOUND(message.panel);
203        if (!panel.sidebarElement || !panel.sidebarPanes)
204            return this._status.E_NOTSUPPORTED();
205        var id = message.id;
206        var sidebar = new WebInspector.ExtensionSidebarPane(message.title, message.id);
207        this._clientObjects[id] = sidebar;
208        panel.sidebarPanes[id] = sidebar;
209        panel.sidebarElement.appendChild(sidebar.element);
210
211        return this._status.OK();
212    },
213
214    createClientIframe: function(parent, url)
215    {
216        var iframe = document.createElement("iframe");
217        iframe.src = url;
218        iframe.style.width = "100%";
219        parent.appendChild(iframe);
220        return iframe;
221    },
222
223    _onSetSidebarHeight: function(message)
224    {
225        var sidebar = this._clientObjects[message.id];
226        if (!sidebar)
227            return this._status.E_NOTFOUND(message.id);
228        sidebar.bodyElement.firstChild.style.height = message.height;
229    },
230
231    _onSetSidebarContent: function(message)
232    {
233        var sidebar = this._clientObjects[message.id];
234        if (!sidebar)
235            return this._status.E_NOTFOUND(message.id);
236        if (message.evaluateOnPage)
237            sidebar.setExpression(message.expression, message.rootTitle);
238        else
239            sidebar.setObject(message.expression, message.rootTitle);
240    },
241
242    _onSetSidebarPage: function(message)
243    {
244        var sidebar = this._clientObjects[message.id];
245        if (!sidebar)
246            return this._status.E_NOTFOUND(message.id);
247        sidebar.setPage(message.url);
248    },
249
250    _onLog: function(message)
251    {
252        WebInspector.log(message.message);
253    },
254
255    _onReload: function(message)
256    {
257        if (typeof message.userAgent === "string")
258            PageAgent.setUserAgentOverride(message.userAgent);
259
260        PageAgent.reloadPage(false);
261        return this._status.OK();
262    },
263
264    _onEvaluateOnInspectedPage: function(message, port)
265    {
266        function callback(error, resultPayload)
267        {
268            if (error)
269                return;
270            var resultObject = WebInspector.RemoteObject.fromPayload(resultPayload);
271            var result = {};
272            if (resultObject.isError())
273                result.isException = true;
274            result.value = resultObject.description;
275            this._dispatchCallback(message.requestId, port, result);
276        }
277        var evalExpression = "JSON.stringify(eval(unescape('" + escape(message.expression) + "')));";
278        RuntimeAgent.evaluate(evalExpression, "", true, callback.bind(this));
279    },
280
281    _onRevealAndSelect: function(message)
282    {
283        if (message.panelId === "resources" && type === "resource")
284            return this._onRevealAndSelectResource(message);
285        else
286            return this._status.E_NOTSUPPORTED(message.panelId, message.type);
287    },
288
289    _onRevealAndSelectResource: function(message)
290    {
291        var id = message.id;
292        var resource = null;
293
294        resource = this._resourceById(id) || WebInspector.resourceForURL(id);
295        if (!resource)
296            return this._status.E_NOTFOUND(typeof id + ": " + id);
297
298        WebInspector.panels.resources.showResource(resource, message.line);
299        WebInspector.showPanel("resources");
300    },
301
302    _dispatchCallback: function(requestId, port, result)
303    {
304        port.postMessage({ command: "callback", requestId: requestId, result: result });
305    },
306
307    _onGetHAR: function(request)
308    {
309        var harLog = (new WebInspector.HARLog()).build();
310        for (var i = 0; i < harLog.entries.length; ++i)
311            harLog.entries[i]._resourceId = this._resourceId(WebInspector.networkResources[i]);
312        return harLog;
313    },
314
315    _onGetResourceContent: function(message, port)
316    {
317        function onContentAvailable(content, encoded)
318        {
319            var response = {
320                encoding: encoded ? "base64" : "",
321                content: content
322            };
323            this._dispatchCallback(message.requestId, port, response);
324        }
325        var resource = this._resourceById(message.id);
326        if (!resource)
327            return this._status.E_NOTFOUND(message.id);
328        resource.requestContent(onContentAvailable.bind(this));
329    },
330
331    _resourceId: function(resource)
332    {
333        if (!resource._extensionResourceId) {
334            resource._extensionResourceId = ++this._lastResourceId;
335            this._resources[resource._extensionResourceId] = resource;
336        }
337        return resource._extensionResourceId;
338    },
339
340    _resourceById: function(id)
341    {
342        return this._resources[id];
343    },
344
345    _onAddAuditCategory: function(request)
346    {
347        var category = new WebInspector.ExtensionAuditCategory(request.id, request.displayName, request.resultCount);
348        if (WebInspector.panels.audits.getCategory(category.id))
349            return this._status.E_EXISTS(category.id);
350        this._clientObjects[request.id] = category;
351        WebInspector.panels.audits.addCategory(category);
352    },
353
354    _onAddAuditResult: function(request)
355    {
356        var auditResult = this._clientObjects[request.resultId];
357        if (!auditResult)
358            return this._status.E_NOTFOUND(request.resultId);
359        try {
360            auditResult.addResult(request.displayName, request.description, request.severity, request.details);
361        } catch (e) {
362            return e;
363        }
364        return this._status.OK();
365    },
366
367    _onStopAuditCategoryRun: function(request)
368    {
369        var auditRun = this._clientObjects[request.resultId];
370        if (!auditRun)
371            return this._status.E_NOTFOUND(request.resultId);
372        auditRun.cancel();
373    },
374
375    initExtensions: function()
376    {
377        // The networkManager is normally created after the ExtensionServer is constructed, but before initExtensions() is called.
378        WebInspector.networkManager.addEventListener(WebInspector.NetworkManager.EventTypes.ResourceFinished, this._notifyResourceFinished, this);
379
380        InspectorExtensionRegistry.getExtensionsAsync();
381    },
382
383    _addExtensions: function(extensions)
384    {
385        // See ExtensionAPI.js and ExtensionCommon.js for details.
386        InspectorFrontendHost.setExtensionAPI(this._buildExtensionAPIInjectedScript());
387        for (var i = 0; i < extensions.length; ++i) {
388            var extension = extensions[i];
389            try {
390                if (!extension.startPage)
391                    return;
392                var iframe = document.createElement("iframe");
393                iframe.src = extension.startPage;
394                iframe.style.display = "none";
395                document.body.appendChild(iframe);
396            } catch (e) {
397                console.error("Failed to initialize extension " + extension.startPage + ":" + e);
398            }
399        }
400    },
401
402    _buildExtensionAPIInjectedScript: function()
403    {
404        var resourceTypes = {};
405        var resourceTypeProperties = Object.getOwnPropertyNames(WebInspector.Resource.Type);
406        for (var i = 0; i < resourceTypeProperties.length; ++i) {
407             var propName = resourceTypeProperties[i];
408             var propValue = WebInspector.Resource.Type[propName];
409             if (typeof propValue === "number")
410                 resourceTypes[propName] = WebInspector.Resource.Type.toString(propValue);
411        }
412        var platformAPI = WebInspector.buildPlatformExtensionAPI ? WebInspector.buildPlatformExtensionAPI() : "";
413        return "(function(){ " +
414            "var apiPrivate = {};" +
415            "(" + WebInspector.commonExtensionSymbols.toString() + ")(apiPrivate);" +
416            "(" + WebInspector.injectedExtensionAPI.toString() + ").apply(this, arguments);" +
417            platformAPI +
418            "})";
419    },
420
421    _onWindowMessage: function(event)
422    {
423        if (event.data !== "registerExtension")
424            return;
425        var port = event.ports[0];
426        port.addEventListener("message", this._onmessage.bind(this), false);
427        port.start();
428    },
429
430    _onmessage: function(event)
431    {
432        var request = event.data;
433        var result;
434
435        if (request.command in this._handlers)
436            result = this._handlers[request.command](request, event.target);
437        else
438            result = this._status.E_NOTSUPPORTED(request.command);
439
440        if (result && request.requestId)
441            this._dispatchCallback(request.requestId, event.target, result);
442    },
443
444    _registerHandler: function(command, callback)
445    {
446        this._handlers[command] = callback;
447    }
448}
449
450WebInspector.ExtensionServer._statuses =
451{
452    OK: "",
453    E_EXISTS: "Object already exists: %s",
454    E_BADARG: "Invalid argument %s: %s",
455    E_BADARGTYPE: "Invalid type for argument %s: got %s, expected %s",
456    E_NOTFOUND: "Object not found: %s",
457    E_NOTSUPPORTED: "Object does not support requested operation: %s",
458}
459
460WebInspector.ExtensionStatus = function()
461{
462    function makeStatus(code)
463    {
464        var description = WebInspector.ExtensionServer._statuses[code] || code;
465        var details = Array.prototype.slice.call(arguments, 1);
466        var status = { code: code, description: description, details: details };
467        if (code !== "OK") {
468            status.isError = true;
469            console.log("Extension server error: " + String.vsprintf(description, details));
470        }
471        return status;
472    }
473    for (status in WebInspector.ExtensionServer._statuses)
474        this[status] = makeStatus.bind(null, status);
475}
476
477WebInspector.addExtensions = function(extensions)
478{
479    WebInspector.extensionServer._addExtensions(extensions);
480}
481
482WebInspector.extensionServer = new WebInspector.ExtensionServer();
483