Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / extensions / extension_error_overlay.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    * Get the url relative to the main extension url. If the url is
10    * unassociated with the extension, this will be the full url.
11    * @param {string} url The url to make relative.
12    * @param {string} extensionUrl The url for the extension resources, in the
13    *     form "chrome-etxension://<extension_id>/".
14    * @return {string} The url relative to the host.
15    */
16   function getRelativeUrl(url, extensionUrl) {
17     return url.substring(0, extensionUrl.length) == extensionUrl ?
18         url.substring(extensionUrl.length) : url;
19   }
20
21   /**
22    * The RuntimeErrorContent manages all content specifically associated with
23    * runtime errors; this includes stack frames and the context url.
24    * @constructor
25    * @extends {HTMLDivElement}
26    */
27   function RuntimeErrorContent() {
28     var contentArea = $('template-collection-extension-error-overlay').
29         querySelector('.extension-error-overlay-runtime-content').
30         cloneNode(true);
31     contentArea.__proto__ = RuntimeErrorContent.prototype;
32     contentArea.init();
33     return contentArea;
34   }
35
36   /**
37    * The name of the "active" class specific to extension errors (so as to
38    * not conflict with other rules).
39    * @type {string}
40    * @const
41    */
42   RuntimeErrorContent.ACTIVE_CLASS_NAME = 'extension-error-active';
43
44   /**
45    * Determine whether or not we should display the url to the user. We don't
46    * want to include any of our own code in stack traces.
47    * @param {string} url The url in question.
48    * @return {boolean} True if the url should be displayed, and false
49    *     otherwise (i.e., if it is an internal script).
50    */
51   RuntimeErrorContent.shouldDisplayForUrl = function(url) {
52     // All our internal scripts are in the 'extensions::' namespace.
53     return !/^extensions::/.test(url);
54   };
55
56   /**
57    * Send a call to chrome to open the developer tools for an error.
58    * This will call either the bound function in ExtensionErrorHandler or the
59    * API function from developerPrivate, depending on whether this is being
60    * used in the native chrome:extensions page or the Apps Developer Tool.
61    * @see chrome/browser/ui/webui/extensions/extension_error_ui_util.h
62    * @param {Object} args The arguments to pass to openDevTools.
63    * @private
64    */
65   RuntimeErrorContent.openDevtools_ = function(args) {
66     if (chrome.send)
67       chrome.send('extensionErrorOpenDevTools', [args]);
68     else if (chrome.developerPrivate)
69       chrome.developerPrivate.openDevTools(args);
70     else
71       assert(false, 'Cannot call either openDevTools function.');
72   };
73
74   RuntimeErrorContent.prototype = {
75     __proto__: HTMLDivElement.prototype,
76
77     /**
78      * The underlying error whose details are being displayed.
79      * @type {Object}
80      * @private
81      */
82     error_: undefined,
83
84     /**
85      * The URL associated with this extension, i.e. chrome-extension://<id>/.
86      * @type {string}
87      * @private
88      */
89     extensionUrl_: undefined,
90
91     /**
92      * The node of the stack trace which is currently active.
93      * @type {HTMLElement}
94      * @private
95      */
96     currentFrameNode_: undefined,
97
98     /**
99      * Initialize the RuntimeErrorContent for the first time.
100      */
101     init: function() {
102       /**
103        * The stack trace element in the overlay.
104        * @type {HTMLElement}
105        * @private
106        */
107       this.stackTrace_ =
108           this.querySelector('.extension-error-overlay-stack-trace-list');
109       assert(this.stackTrace_);
110
111       /**
112        * The context URL element in the overlay.
113        * @type {HTMLElement}
114        * @private
115        */
116       this.contextUrl_ =
117           this.querySelector('.extension-error-overlay-context-url');
118       assert(this.contextUrl_);
119     },
120
121     /**
122      * Sets the error for the content.
123      * @param {Object} error The error whose content should be displayed.
124      * @param {string} extensionUrl The URL associated with this extension.
125      */
126     setError: function(error, extensionUrl) {
127       this.error_ = error;
128       this.extensionUrl_ = extensionUrl;
129       this.contextUrl_.textContent = error.contextUrl ?
130           getRelativeUrl(error.contextUrl, this.extensionUrl_) :
131           loadTimeData.getString('extensionErrorOverlayContextUnknown');
132       this.initStackTrace_();
133     },
134
135     /**
136      * Wipe content associated with a specific error.
137      */
138     clearError: function() {
139       this.error_ = undefined;
140       this.extensionUrl_ = undefined;
141       this.currentFrameNode_ = undefined;
142       this.stackTrace_.innerHTML = '';
143       this.stackTrace_.hidden = true;
144     },
145
146     /**
147      * Makes |frame| active and deactivates the previously active frame (if
148      * there was one).
149      * @param {HTMLElement} frame The frame to activate.
150      * @private
151      */
152     setActiveFrame_: function(frameNode) {
153       if (this.currentFrameNode_) {
154         this.currentFrameNode_.classList.remove(
155             RuntimeErrorContent.ACTIVE_CLASS_NAME);
156       }
157
158       this.currentFrameNode_ = frameNode;
159       this.currentFrameNode_.classList.add(
160           RuntimeErrorContent.ACTIVE_CLASS_NAME);
161     },
162
163     /**
164      * Initialize the stack trace element of the overlay.
165      * @private
166      */
167     initStackTrace_: function() {
168       for (var i = 0; i < this.error_.stackTrace.length; ++i) {
169         var frame = this.error_.stackTrace[i];
170         // Don't include any internal calls (e.g., schemaBindings) in the
171         // stack trace.
172         if (!RuntimeErrorContent.shouldDisplayForUrl(frame.url))
173           continue;
174
175         var frameNode = document.createElement('li');
176         // Attach the index of the frame to which this node refers (since we
177         // may skip some, this isn't a 1-to-1 match).
178         frameNode.indexIntoTrace = i;
179
180         // The description is a human-readable summation of the frame, in the
181         // form "<relative_url>:<line_number> (function)", e.g.
182         // "myfile.js:25 (myFunction)".
183         var description = getRelativeUrl(frame.url, this.extensionUrl_) +
184                           ':' + frame.lineNumber;
185         if (frame.functionName) {
186           var functionName = frame.functionName == '(anonymous function)' ?
187               loadTimeData.getString('extensionErrorOverlayAnonymousFunction') :
188               frame.functionName;
189           description += ' (' + functionName + ')';
190         }
191         frameNode.textContent = description;
192
193         // When the user clicks on a frame in the stack trace, we should
194         // highlight that overlay in the list, display the appropriate source
195         // code with the line highlighted, and link the "Open DevTools" button
196         // with that frame.
197         frameNode.addEventListener('click', function(frame, frameNode, e) {
198           if (this.currStackFrame_ == frameNode)
199             return;
200
201           this.setActiveFrame_(frameNode);
202
203           // Request the file source with the section highlighted; this will
204           // call ExtensionErrorOverlay.requestFileSourceResponse() when
205           // completed, which in turn calls setCode().
206           ExtensionErrorOverlay.requestFileSource(
207               {extensionId: this.error_.extensionId,
208                message: this.error_.message,
209                pathSuffix: getRelativeUrl(frame.url, this.extensionUrl_),
210                lineNumber: frame.lineNumber});
211         }.bind(this, frame, frameNode));
212
213         this.stackTrace_.appendChild(frameNode);
214       }
215
216       // Set the current stack frame to the first stack frame and show the
217       // trace, if one exists. (We can't just check error.stackTrace, because
218       // it's possible the trace was purely internal, and we don't show
219       // internal frames.)
220       if (this.stackTrace_.children.length > 0) {
221         this.stackTrace_.hidden = false;
222         this.setActiveFrame_(this.stackTrace_.firstChild);
223       }
224     },
225
226     /**
227      * Open the developer tools for the active stack frame.
228      */
229     openDevtools: function() {
230       var stackFrame =
231           this.error_.stackTrace[this.currentFrameNode_.indexIntoTrace];
232
233       RuntimeErrorContent.openDevtools_(
234           {renderProcessId: this.error_.renderProcessId,
235            renderViewId: this.error_.renderViewId,
236            url: stackFrame.url,
237            lineNumber: stackFrame.lineNumber || 0,
238            columnNumber: stackFrame.columnNumber || 0});
239     }
240   };
241
242   /**
243    * The ExtensionErrorOverlay will show the contents of a file which pertains
244    * to the ExtensionError; this is either the manifest file (for manifest
245    * errors) or a source file (for runtime errors). If possible, the portion
246    * of the file which caused the error will be highlighted.
247    * @constructor
248    */
249   function ExtensionErrorOverlay() {
250     /**
251      * The content section for runtime errors; this is re-used for all
252      * runtime errors and attached/detached from the overlay as needed.
253      * @type {RuntimeErrorContent}
254      * @private
255      */
256     this.runtimeErrorContent_ = new RuntimeErrorContent();
257   }
258
259   /**
260    * Value of ExtensionError::RUNTIME_ERROR enum.
261    * @see extensions/browser/extension_error.h
262    * @type {number}
263    * @const
264    * @private
265    */
266   ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_ = 1;
267
268   /**
269    * The manifest filename.
270    * @type {string}
271    * @const
272    * @private
273    */
274   ExtensionErrorOverlay.MANIFEST_FILENAME_ = 'manifest.json';
275
276   /**
277    * Determine whether or not chrome can load the source for a given file; this
278    * can only be done if the file belongs to the extension.
279    * @param {string} file The file to load.
280    * @param {string} extensionUrl The url for the extension, in the form
281    *     chrome-extension://<extension-id>/.
282    * @return {boolean} True if the file can be loaded, false otherwise.
283    * @private
284    */
285   ExtensionErrorOverlay.canLoadFileSource = function(file, extensionUrl) {
286     return file.substr(0, extensionUrl.length) == extensionUrl ||
287            file.toLowerCase() == ExtensionErrorOverlay.MANIFEST_FILENAME_;
288   };
289
290   /**
291    * Determine whether or not we can show an overlay with more details for
292    * the given extension error.
293    * @param {Object} error The extension error.
294    * @param {string} extensionUrl The url for the extension, in the form
295    *     "chrome-extension://<extension-id>/".
296    * @return {boolean} True if we can show an overlay for the error,
297    *     false otherwise.
298    */
299   ExtensionErrorOverlay.canShowOverlayForError = function(error, extensionUrl) {
300     if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl))
301       return true;
302
303     if (error.stackTrace) {
304       for (var i = 0; i < error.stackTrace.length; ++i) {
305         if (RuntimeErrorContent.shouldDisplayForUrl(error.stackTrace[i].url))
306           return true;
307       }
308     }
309
310     return false;
311   };
312
313   /**
314    * Send a call to chrome to request the source of a given file.
315    * This will call either the bound function in ExtensionErrorHandler or the
316    * API function from developerPrivate, depending on whether this is being
317    * used in the native chrome:extensions page or the Apps Developer Tool.
318    * @see chrome/browser/ui/webui/extensions/extension_error_ui_util.h
319    * @param {Object} args The arguments to pass to requestFileSource.
320    */
321   ExtensionErrorOverlay.requestFileSource = function(args) {
322     if (chrome.send) {
323       chrome.send('extensionErrorRequestFileSource', [args]);
324     } else if (chrome.developerPrivate) {
325       chrome.developerPrivate.requestFileSource(args, function(result) {
326         extensions.ExtensionErrorOverlay.requestFileSourceResponse(result);
327       });
328     } else {
329       assert(false, 'Cannot call either requestFileSource function.');
330     }
331   };
332
333   cr.addSingletonGetter(ExtensionErrorOverlay);
334
335   ExtensionErrorOverlay.prototype = {
336     /**
337      * The underlying error whose details are being displayed.
338      * @type {Object}
339      * @private
340      */
341     error_: undefined,
342
343     /**
344      * Initialize the page.
345      * @param {function(HTMLDivElement)} showOverlay The function to show or
346      *     hide the ExtensionErrorOverlay; this should take a single parameter
347      *     which is either the overlay Div if the overlay should be displayed,
348      *     or null if the overlay should be hidden.
349      */
350     initializePage: function(showOverlay) {
351       var overlay = $('overlay');
352       cr.ui.overlay.setupOverlay(overlay);
353       cr.ui.overlay.globalInitialization();
354       overlay.addEventListener('cancelOverlay', this.handleDismiss_.bind(this));
355
356       $('extension-error-overlay-dismiss').addEventListener(
357           'click', this.handleDismiss_.bind(this));
358
359       /**
360        * The element of the full overlay.
361        * @type {HTMLDivElement}
362        * @private
363        */
364       this.overlayDiv_ = $('extension-error-overlay');
365
366       /**
367        * The portion of the overlay which shows the code relating to the error.
368        * @type {HTMLElement}
369        * @private
370        */
371       this.codeDiv_ = $('extension-error-overlay-code');
372
373       /**
374        * The function to show or hide the ExtensionErrorOverlay.
375        * @type {function}
376        * @param {boolean} isVisible Whether the overlay should be visible.
377        */
378       this.setVisible = function(isVisible) {
379         showOverlay(isVisible ? this.overlayDiv_ : null);
380       };
381
382       /**
383        * The button to open the developer tools (only available for runtime
384        * errors).
385        * @type {HTMLButtonElement}
386        * @private
387        */
388       this.openDevtoolsButton_ = $('extension-error-overlay-devtools-button');
389       this.openDevtoolsButton_.addEventListener('click', function() {
390           this.runtimeErrorContent_.openDevtools();
391       }.bind(this));
392     },
393
394     /**
395      * Handles a click on the dismiss ("OK" or close) buttons.
396      * @param {Event} e The click event.
397      * @private
398      */
399     handleDismiss_: function(e) {
400       this.setVisible(false);
401
402       // There's a chance that the overlay receives multiple dismiss events; in
403       // this case, handle it gracefully and return (since all necessary work
404       // will already have been done).
405       if (!this.error_)
406         return;
407
408       this.codeDiv_.innerHTML = '';
409       this.openDevtoolsButton_.hidden = true;
410
411       if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) {
412         this.overlayDiv_.querySelector('.content-area').removeChild(
413             this.runtimeErrorContent_);
414         this.runtimeErrorContent_.clearError();
415       }
416
417       this.error_ = undefined;
418     },
419
420     /**
421      * Associate an error with the overlay. This will set the error for the
422      * overlay, and, if possible, will populate the code section of the overlay
423      * with the relevant file, load the stack trace, and generate links for
424      * opening devtools (the latter two only happen for runtime errors).
425      * @param {Object} error The error to show in the overlay.
426      * @param {string} extensionUrl The URL of the extension, in the form
427      *     "chrome-extension://<extension_id>".
428      */
429     setErrorAndShowOverlay: function(error, extensionUrl) {
430       this.error_ = error;
431
432       if (this.error_.type == ExtensionErrorOverlay.RUNTIME_ERROR_TYPE_) {
433         this.runtimeErrorContent_.setError(this.error_, extensionUrl);
434         this.overlayDiv_.querySelector('.content-area').insertBefore(
435             this.runtimeErrorContent_,
436             this.codeDiv_.nextSibling);
437         this.openDevtoolsButton_.hidden = false;
438         this.openDevtoolsButton_.disabled = !error.canInspect;
439       }
440
441       if (ExtensionErrorOverlay.canLoadFileSource(error.source, extensionUrl)) {
442         var relativeUrl = getRelativeUrl(error.source, extensionUrl);
443
444         var requestFileSourceArgs = {extensionId: error.extensionId,
445                                      message: error.message,
446                                      pathSuffix: relativeUrl};
447
448         if (relativeUrl.toLowerCase() ==
449                 ExtensionErrorOverlay.MANIFEST_FILENAME_) {
450           requestFileSourceArgs.manifestKey = error.manifestKey;
451           requestFileSourceArgs.manifestSpecific = error.manifestSpecific;
452         } else {
453           requestFileSourceArgs.lineNumber =
454               error.stackTrace && error.stackTrace[0] ?
455                   error.stackTrace[0].lineNumber : 0;
456         }
457         ExtensionErrorOverlay.requestFileSource(requestFileSourceArgs);
458       } else {
459         ExtensionErrorOverlay.requestFileSourceResponse(null);
460       }
461     },
462
463     /**
464      * Set the code to be displayed in the code portion of the overlay.
465      * @see ExtensionErrorOverlay.requestFileSourceResponse().
466      * @param {?Object} code The code to be displayed. If |code| is null, then
467      *     a "Could not display code" message will be displayed instead.
468      */
469     setCode: function(code) {
470       document.querySelector(
471           '#extension-error-overlay .extension-error-overlay-title').
472               textContent = code.title;
473       this.codeDiv_.innerHTML = '';
474
475       // If there's no code, then display an appropriate message.
476       if (!code) {
477         var span = document.createElement('span');
478         span.textContent =
479             loadTimeData.getString('extensionErrorOverlayNoCodeToDisplay');
480         this.codeDiv_.appendChild(span);
481         return;
482       }
483
484       var createSpan = function(source, isHighlighted) {
485         var span = document.createElement('span');
486         span.className = isHighlighted ? 'highlighted-source' : 'normal-source';
487         source = source.replace(/ /g, '&nbsp;').replace(/\n|\r/g, '<br>');
488         span.innerHTML = source;
489         return span;
490       };
491
492       if (code.beforeHighlight)
493         this.codeDiv_.appendChild(createSpan(code.beforeHighlight, false));
494
495       if (code.highlight) {
496         var highlightSpan = createSpan(code.highlight, true);
497         highlightSpan.title = code.message;
498         this.codeDiv_.appendChild(highlightSpan);
499       }
500
501       if (code.afterHighlight)
502         this.codeDiv_.appendChild(createSpan(code.afterHighlight, false));
503     },
504   };
505
506   /**
507    * Called by the ExtensionErrorHandler responding to the request for a file's
508    * source. Populate the content area of the overlay and display the overlay.
509    * @param {Object?} result An object with four strings - the title,
510    *     beforeHighlight, afterHighlight, and highlight. The three 'highlight'
511    *     strings represent three portions of the file's content to display - the
512    *     portion which is most relevant and should be emphasized (highlight),
513    *     and the parts both before and after this portion. These may be empty.
514    */
515   ExtensionErrorOverlay.requestFileSourceResponse = function(result) {
516     var overlay = extensions.ExtensionErrorOverlay.getInstance();
517     overlay.setCode(result);
518     overlay.setVisible(true);
519   };
520
521   // Export
522   return {
523     ExtensionErrorOverlay: ExtensionErrorOverlay
524   };
525 });