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