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