- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / extensions / extension_error.js
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 cr.define('extensions', function() {
6   'use strict';
7
8   /**
9    * Returns whether or not a given |url| is associated with an extension.
10    * @param {string} url The url to examine.
11    * @param {string} extensionUrl The url of the extension.
12    * @return {boolean} Whether or not the url is associated with the extension.
13    */
14   function isExtensionUrl(url, extensionUrl) {
15     return url.substring(0, extensionUrl.length) == extensionUrl;
16   }
17
18   /**
19    * Get the url relative to the main extension url. If the url is
20    * unassociated with the extension, this will be the full url.
21    * @param {string} url The url to make relative.
22    * @param {string} extensionUrl The host for which the url is relative.
23    * @return {string} The url relative to the host.
24    */
25   function getRelativeUrl(url, extensionUrl) {
26     return isExtensionUrl(url, extensionUrl) ?
27         url.substring(extensionUrl.length) : url;
28   }
29
30   /**
31    * Clone a template within the extension error template collection.
32    * @param {string} templateName The class name of the template to clone.
33    * @return {HTMLElement} The clone of the template.
34    */
35   function cloneTemplate(templateName) {
36     return $('template-collection-extension-error').
37         querySelector('.' + templateName).cloneNode(true);
38   }
39
40   /**
41    * Creates a new ExtensionError HTMLElement; this is used to show a
42    * notification to the user when an error is caused by an extension.
43    * @param {Object} error The error the element should represent.
44    * @param {string} templateName The name of the template to clone for the
45    *     error ('extension-error-[detailed|simple]-wrapper').
46    * @constructor
47    * @extends {HTMLDivElement}
48    */
49   function ExtensionError(error, templateName) {
50     var div = cloneTemplate(templateName);
51     div.__proto__ = ExtensionError.prototype;
52     div.error_ = error;
53     div.decorate();
54     return div;
55   }
56
57   ExtensionError.prototype = {
58     __proto__: HTMLDivElement.prototype,
59
60     /** @override */
61     decorate: function() {
62       var metadata = cloneTemplate('extension-error-metadata');
63
64       // Add an additional class for the severity level.
65       if (this.error_.level == 0)
66         metadata.classList.add('extension-error-severity-info');
67       else if (this.error_.level == 1)
68         metadata.classList.add('extension-error-severity-warning');
69       else
70         metadata.classList.add('extension-error-severity-fatal');
71
72       var iconNode = document.createElement('img');
73       iconNode.className = 'extension-error-icon';
74       metadata.insertBefore(iconNode, metadata.firstChild);
75
76       // Add a property for the extension's base url in order to determine if
77       // a url belongs to the extension.
78       this.extensionUrl_ =
79           'chrome-extension://' + this.error_.extensionId + '/';
80
81       metadata.querySelector('.extension-error-message').textContent =
82           this.error_.message;
83
84       metadata.appendChild(this.createViewSourceAndInspect_(
85           getRelativeUrl(this.error_.source, this.extensionUrl_),
86           this.error_.source));
87
88       // The error template may specify a <summary> to put template metadata in.
89       // If not, just append it to the top-level element.
90       var metadataContainer = this.querySelector('summary') || this;
91       metadataContainer.appendChild(metadata);
92
93       var detailsNode = this.querySelector('.extension-error-details');
94       if (detailsNode && this.error_.contextUrl)
95         detailsNode.appendChild(this.createContextNode_());
96       if (detailsNode && this.error_.stackTrace) {
97         var stackNode = this.createStackNode_();
98         if (stackNode)
99           detailsNode.appendChild(this.createStackNode_());
100       }
101     },
102
103     /**
104      * Return a div with text |description|. If it's possible to view the source
105      * for |url|, linkify the div to do so. Attach an inspect button if it's
106      * possible to open the inspector for |url|.
107      * @param {string} description a human-friendly description the location
108      *     (e.g., filename, line).
109      * @param {string} url The url of the resource to view.
110      * @param {?number} line An optional line number of the resource.
111      * @param {?number} column An optional column number of the resource.
112      * @return {HTMLElement} The created node, either a link or plaintext.
113      * @private
114      */
115     createViewSourceAndInspect_: function(description, url, line, column) {
116       var errorLinks = document.createElement('div');
117       errorLinks.className = 'extension-error-links';
118
119       if (this.error_.canInspect)
120         errorLinks.appendChild(this.createInspectLink_(url, line, column));
121
122       if (this.canViewSource_(url))
123         var viewSource = this.createViewSourceLink_(url, line);
124       else
125         var viewSource = document.createElement('div');
126       viewSource.className = 'extension-error-view-source';
127       viewSource.textContent = description;
128       errorLinks.appendChild(viewSource);
129       return errorLinks;
130     },
131
132     /**
133      * Determine whether we can view the source of a given url.
134      * @param {string} url The url of the resource to view.
135      * @return {boolean} Whether or not we can view the source for the url.
136      * @private
137      */
138     canViewSource_: function(url) {
139       return isExtensionUrl(url, this.extensionUrl_) || url == 'manifest.json';
140     },
141
142     /**
143      * Determine whether or not we should display the url to the user. We don't
144      * want to include any of our own code in stack traces.
145      * @param {string} url The url in question.
146      * @return {boolean} True if the url should be displayed, and false
147      *     otherwise (i.e., if it is an internal script).
148      */
149     shouldDisplayForUrl_: function(url) {
150       var extensionsNamespace = 'extensions::';
151       // All our internal scripts are in the 'extensions::' namespace.
152       return url.substr(0, extensionsNamespace.length) != extensionsNamespace;
153     },
154
155     /**
156      * Create a clickable node to view the source for the given url.
157      * @param {string} url The url to the resource to view.
158      * @param {?number} line An optional line number of the resource (for
159      *     source files).
160      * @return {HTMLElement} The clickable node to view the source.
161      * @private
162      */
163     createViewSourceLink_: function(url, line) {
164       var viewSource = document.createElement('a');
165       viewSource.href = 'javascript:void(0)';
166       var relativeUrl = getRelativeUrl(url, this.extensionUrl_);
167       var requestFileSourceArgs = { 'extensionId': this.error_.extensionId,
168                                     'message': this.error_.message,
169                                     'pathSuffix': relativeUrl };
170       if (relativeUrl == 'manifest.json') {
171         requestFileSourceArgs.manifestKey = this.error_.manifestKey;
172         requestFileSourceArgs.manifestSpecific = this.error_.manifestSpecific;
173       } else {
174         // Prefer |line| if available, or default to the line of the last stack
175         // frame.
176         requestFileSourceArgs.lineNumber =
177             line ? line : this.getLastPosition_('lineNumber');
178       }
179
180       viewSource.addEventListener('click', function(e) {
181         chrome.send('extensionErrorRequestFileSource', [requestFileSourceArgs]);
182       });
183       viewSource.title = loadTimeData.getString('extensionErrorViewSource');
184       return viewSource;
185     },
186
187     /**
188      * Check the most recent stack frame to get the last position in the code.
189      * @param {string} type The position type, i.e. '[line|column]Number'.
190      * @return {?number} The last position of the given |type|, or undefined if
191      *     there is no stack trace to check.
192      * @private
193      */
194     getLastPosition_: function(type) {
195       var stackTrace = this.error_.stackTrace;
196       return stackTrace && stackTrace[0] ? stackTrace[0][type] : undefined;
197     },
198
199     /**
200      * Create an "Inspect" link, in the form of an icon.
201      * @param {?string} url The url of the resource to inspect; if absent, the
202      *     render view (and no particular resource) is inspected.
203      * @param {?number} line An optional line number of the resource.
204      * @param {?number} column An optional column number of the resource.
205      * @return {HTMLImageElement} The created "Inspect" link for the resource.
206      * @private
207      */
208     createInspectLink_: function(url, line, column) {
209       var linkWrapper = document.createElement('a');
210       linkWrapper.href = 'javascript:void(0)';
211       var inspectIcon = document.createElement('img');
212       inspectIcon.className = 'extension-error-inspect';
213       inspectIcon.title = loadTimeData.getString('extensionErrorInspect');
214
215       inspectIcon.addEventListener('click', function(e) {
216           chrome.send('extensionErrorOpenDevTools',
217                       [{'renderProcessId': this.error_.renderProcessId,
218                         'renderViewId': this.error_.renderViewId,
219                         'url': url,
220                         'lineNumber': line ? line :
221                             this.getLastPosition_('lineNumber'),
222                         'columnNumber': column ? column :
223                             this.getLastPosition_('columnNumber')}]);
224       }.bind(this));
225       linkWrapper.appendChild(inspectIcon);
226       return linkWrapper;
227     },
228
229     /**
230      * Get the context node for this error. This will attempt to link to the
231      * context in which the error occurred, and can be either an extension page
232      * or an external page.
233      * @return {HTMLDivElement} The context node for the error, including the
234      *     label and a link to the context.
235      * @private
236      */
237     createContextNode_: function() {
238       var node = cloneTemplate('extension-error-context-wrapper');
239       var linkNode = node.querySelector('a');
240       if (isExtensionUrl(this.error_.contextUrl, this.extensionUrl_)) {
241         linkNode.textContent = getRelativeUrl(this.error_.contextUrl,
242                                               this.extensionUrl_);
243       } else {
244         linkNode.textContent = this.error_.contextUrl;
245       }
246
247       // Prepend a link to inspect the context page, if possible.
248       if (this.error_.canInspect)
249         node.insertBefore(this.createInspectLink_(), linkNode);
250
251       linkNode.href = this.error_.contextUrl;
252       linkNode.target = '_blank';
253       return node;
254     },
255
256     /**
257      * Get a node for the stack trace for this error. Each stack frame will
258      * include a resource url, line number, and function name (possibly
259      * anonymous). If possible, these frames will also be linked for viewing the
260      * source and inspection.
261      * @return {HTMLDetailsElement} The stack trace node for this error, with
262      *     all stack frames nested in a details-summary object.
263      * @private
264      */
265     createStackNode_: function() {
266       var node = cloneTemplate('extension-error-stack-trace');
267       var listNode = node.querySelector('.extension-error-stack-trace-list');
268       this.error_.stackTrace.forEach(function(frame) {
269         if (!this.shouldDisplayForUrl_(frame.url))
270           return;
271         var frameNode = document.createElement('div');
272         var description = getRelativeUrl(frame.url, this.extensionUrl_) +
273                           ':' + frame.lineNumber;
274         if (frame.functionName) {
275           var functionName = frame.functionName == '(anonymous function)' ?
276               loadTimeData.getString('extensionErrorAnonymousFunction') :
277               frame.functionName;
278           description += ' (' + functionName + ')';
279         }
280         frameNode.appendChild(this.createViewSourceAndInspect_(
281             description, frame.url, frame.lineNumber, frame.columnNumber));
282         listNode.appendChild(
283             document.createElement('li')).appendChild(frameNode);
284       }, this);
285
286       if (listNode.childElementCount == 0)
287         return undefined;
288
289       return node;
290     },
291   };
292
293   /**
294    * A variable length list of runtime or manifest errors for a given extension.
295    * @param {Array.<Object>} errors The list of extension errors with which
296    *     to populate the list.
297    * @param {string} title The i18n key for the title of the error list, i.e.
298    *     'extensionErrors[Manifest,Runtime]Errors'.
299    * @constructor
300    * @extends {HTMLDivElement}
301    */
302   function ExtensionErrorList(errors, title) {
303     var div = cloneTemplate('extension-error-list');
304     div.__proto__ = ExtensionErrorList.prototype;
305     div.errors_ = errors;
306     div.title_ = title;
307     div.decorate();
308     return div;
309   }
310
311   ExtensionErrorList.prototype = {
312     __proto__: HTMLDivElement.prototype,
313
314     /**
315      * @private
316      * @const
317      * @type {number}
318      */
319     MAX_ERRORS_TO_SHOW_: 3,
320
321     /** @override */
322     decorate: function() {
323       this.querySelector('.extension-error-list-title').textContent =
324           loadTimeData.getString(this.title_);
325
326       this.contents_ = this.querySelector('.extension-error-list-contents');
327       this.errors_.forEach(function(error) {
328         this.contents_.appendChild(document.createElement('li')).appendChild(
329             new ExtensionError(error,
330                                error.contextUrl || error.stackTrace ?
331                                    'extension-error-detailed-wrapper' :
332                                    'extension-error-simple-wrapper'));
333       }, this);
334
335       if (this.contents_.children.length > this.MAX_ERRORS_TO_SHOW_) {
336         for (var i = this.MAX_ERRORS_TO_SHOW_;
337              i < this.contents_.children.length; ++i) {
338           this.contents_.children[i].hidden = true;
339         }
340         this.initShowMoreButton_();
341       }
342     },
343
344     /**
345      * Initialize the "Show More" button for the error list. If there are more
346      * than |MAX_ERRORS_TO_SHOW_| errors in the list.
347      * @private
348      */
349     initShowMoreButton_: function() {
350       var button = this.querySelector('.extension-error-list-show-more a');
351       button.hidden = false;
352       button.isShowingAll = false;
353       button.addEventListener('click', function(e) {
354         for (var i = this.MAX_ERRORS_TO_SHOW_;
355              i < this.contents_.children.length; ++i) {
356           this.contents_.children[i].hidden = button.isShowingAll;
357         }
358         var message = button.isShowingAll ? 'extensionErrorsShowMore' :
359                                             'extensionErrorsShowFewer';
360         button.textContent = loadTimeData.getString(message);
361         button.isShowingAll = !button.isShowingAll;
362       }.bind(this));
363     }
364   };
365
366   return {
367     ExtensionErrorList: ExtensionErrorList
368   };
369 });