Bugreport url as constant
[tine20] / tine20 / Tinebase / js / ExceptionDialog.js
1 /*
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Cornelius Weiss <c.weiss@metaways.de>
6  * @copyright   Copyright (c) 2007-2012 Metaways Infosystems GmbH (http://www.metaways.de)
7  *
8  */
9
10 /*global Ext, Tine, window*/
11
12 Ext.ns('Tine', 'Tine.Tinebase');
13  
14  /**
15   * @namespace  Tine.Tinebase
16   * @class      Tine.Tinebase.ExceptionDialog
17   * @extends    Ext.Window
18   */
19 Tine.Tinebase.ExceptionDialog = Ext.extend(Ext.Window, {
20     
21     width: 400,
22     height: 600,
23     xtype: 'panel',
24     layout: 'fit',
25     plain: true,
26     closeAction: 'close',
27     autoScroll: true,
28     releaseMode: true,
29     
30     /**
31      * non-interactive mode: do not show anything and just submit the report
32      * 
33      * @type Boolean
34      */
35     nonInteractive: false,
36     
37     /**
38      * @private
39      */
40     initComponent: function () {
41         this.currentAccount = Tine.Tinebase.registry.get('currentAccount');
42         
43         // check if we have the version in registry (is not the case in the setup)
44         if (! Tine.Tinebase.registry.get('version') || Tine.Tinebase.registry.get('version').buildType != 'RELEASE') {
45             this.releaseMode = false;
46             this.width = 800;
47         }
48         
49         var trace = '';
50         if (Ext.isArray(this.exception.trace)) {
51             for (var i = 0,j = this.exception.trace.length; i < j; i++) {
52                 trace += (this.exception.trace[i].file ? this.exception.trace[i].file : '[internal function]') +
53                          (this.exception.trace[i].line ? '(' + this.exception.trace[i].line + ')' : '') + ': ' +
54                          (this.exception.trace[i]['class'] ? '<b>' + this.exception.trace[i]['class'] + this.exception.trace[i].type + '</b>' : '') +
55                          '<b>' + this.exception.trace[i]['function'] + '</b>' +
56                         '(' + ((this.exception.trace[i].args && this.exception.trace[i].args[0]) ? this.exception.trace[i].args[0] : '') + ')<br/>';
57             }
58             
59             this.exception.traceHTML = trace;
60         }
61         
62         this.title = _('Abnormal End');
63         this.items = this.getReportForm();
64         
65         Tine.Tinebase.ExceptionDialog.superclass.initComponent.call(this);
66         
67         this.on('show', function () {
68             if (this.nonInteractive) {
69                 Tine.log.debug('Tine.Tinebase.ExceptionDialog::onShow -> Sending bugreport non-interactive ...');
70                 this.onSendReport();
71             } else {
72                 // fix layout issue
73                 this.setHeight(this.getHeight() + 10);
74             }
75         }, this);
76         
77         this.on('close', function() {
78             if (Tine.Tinebase.configManager.get('automaticBugreports') && ! this.nonInteractive) {
79                 Tine.log.debug('Tine.Tinebase.ExceptionDialog::onCancel -> Activate non-interacive exception dialog.');
80                 Tine.Tinebase.exceptionDlg = new Tine.Tinebase.ExceptionDialog({
81                     exception: this.exception,
82                     nonInteractive: true,
83                     listeners: {
84                         close: function() {
85                             Tine.Tinebase.exceptionDlg = null;
86                         }
87                     }
88                 });
89                 Tine.Tinebase.exceptionDlg.show();
90             }
91         }, this);
92     },
93     
94     /**
95      * use button order based on preference
96      * 
97      * @private
98      * @return {Array}
99      */
100     initButtons: function () {
101         if (Tine.Tinebase.registry && Tine.Tinebase.registry.get('preferences') && Tine.Tinebase.registry.get('preferences').get('dialogButtonsOrderStyle') === 'Windows') {
102             this.reportButtons = [{
103                 text: _('Send Report'),
104                 iconCls: 'action_saveAndClose',
105                 enabled: Tine.Tinebase.common.hasRight('report_bugs', 'Tinebase'),
106                 scope: this,
107                 handler: this.onSendReport
108             }, {
109                 text: _('Cancel'),
110                 iconCls: 'action_cancel',
111                 scope: this,
112                 handler: function() {
113                     this.close();
114                 }
115             }];
116         }
117         else {
118             this.reportButtons = [{
119                 text: _('Cancel'),
120                 iconCls: 'action_cancel',
121                 scope: this,
122                 handler: function() {
123                     this.close();
124                 }
125             }, {
126                 text: _('Send Report'),
127                 iconCls: 'action_saveAndClose',
128                 enabled: Tine.Tinebase.common.hasRight('report_bugs', 'Tinebase'),
129                 scope: this,
130                 handler: this.onSendReport
131             }];
132         }
133         
134         return this.reportButtons;
135     },
136     
137     /**
138      * @private
139      */
140     getReportForm: function () {
141         this.initButtons();
142         
143         this.reportForm = new Ext.FormPanel({
144             id: 'tb-exceptiondialog-frompanel',
145             bodyStyle: 'padding:5px;',
146             buttonAlign: 'right',
147             labelAlign: 'top',
148             autoScroll: true,
149             buttons: this.reportButtons,
150             items: [{
151                 xtype: 'panel',
152                 border: false,
153                 html: '<div class="tb-exceptiondialog-text">' + 
154                         '<p>' + _('An error occurred, the program ended abnormal.') + '</p>' +
155                         '<p>' + _('The last action you made was potentially not performed correctly.') + '</p>' +
156                         '<p>' + _('Please help improving this software and notify the vendor. Include a brief description of what you where doing when the error occurred.') + '</p>' + 
157                     '</div>'
158             }, {
159                 id: 'tb-exceptiondialog-description',
160                 height: 60,
161                 xtype: 'textarea',
162                 fieldLabel: _('Description'),
163                 name: 'description',
164                 anchor: '95%',
165                 readOnly: false
166             }, {
167                 xtype: 'fieldset',
168                 id: 'tb-exceptiondialog-send-contact',
169                 anchor: '95%',
170                 title: _('Send Contact Information'),
171                 autoHeight: true,
172                 checkboxToggle: true,
173                 items: [{
174                     id: 'tb-exceptiondialog-contact',
175                     xtype: 'textfield',
176                     hideLabel: true,
177                     anchor: '100%',
178                     name: 'contact',
179                     value: (this.currentAccount) ? this.currentAccount.accountFullName + ' ' + this.currentAccount.accountEmailAddress : 'unknown'
180                 }]
181             }, {
182                 xtype: 'panel',
183                 width: '95%',
184                 layout: 'form',
185                 collapsible: true,
186                 collapsed: this.releaseMode,
187                 title: _('Details:'),
188                 defaults: {
189                     xtype: 'textfield',
190                     readOnly: true,
191                     anchor: '95%'
192                 },
193                 html: '<div class="tb-exceptiondialog-details">' +
194                         '<p class="tb-exceptiondialog-msg">' + this.exception.message + '</p>' +
195                         '<p class="tb-exceptiondialog-trace">' + this.exception.traceHTML + '</p>' +
196                     '</div>'
197             }]
198         });
199         
200         return this.reportForm;
201     },
202     
203     /**
204      * send the report to tine20.org bugracker
205      * 
206      * @private
207      */
208     onSendReport: function () {
209         if (! this.nonInteractive) {
210             Ext.MessageBox.wait(_('Sending report...'), _('Please wait a moment'));
211         }
212         var baseUrl = Tine.bugreportUrl;
213         var hash = this.generateHash();
214         
215         this.exception.msg           = this.exception.message;
216         this.exception.description   = Ext.getCmp('tb-exceptiondialog-description').getValue();
217         this.exception.clientVersion = Tine.clientVersion;
218         this.exception.serverVersion = (Tine.Tinebase.registry.get('version')) ? Tine.Tinebase.registry.get('version') : {};
219         
220         // tinebase version
221         Ext.each(Tine.Tinebase.registry.get('userApplications'), function (app) {
222             if (app.name == 'Tinebase') {
223                 this.exception.tinebaseVersion = app;
224                 return false;
225             }
226         }, this);
227         
228         // append contact?
229         if (! Ext.getCmp('tb-exceptiondialog-send-contact').collapsed) {
230             this.exception.contact = Ext.getCmp('tb-exceptiondialog-contact').getValue();
231         }
232
233
234         var ua = navigator.userAgent.toLowerCase(),
235             isIE = ua.match(/msie (\d+)/),
236             useCOSR = !isIE || isIE[1] > 9;
237
238         if (useCOSR) {
239             // NOTE: - we create a new connection to not compromise the json key
240             var conn = new Ext.data.Connection();
241             conn.request({
242                 url: baseUrl,
243                 method: 'POST',
244                 jsonData: Ext.encode({
245                     jsonrpc: '2.0',
246                     method: 'API.reportBug',
247                     id: ++Ext.Ajax.requestId,
248                     params: {
249                         hash: hash,
250                         exception: this.exception
251                     }
252                 })
253             });
254         } else {
255             // NOTE:  - we have about 80 chars overhead (url, paramnames etc) in each request
256             //        - 1024 chars are expected to be pass client/server limits savely => 940
257             //        - base64 means about 30% overhead => 600
258             var chunks = this.strChunk(Ext.util.JSON.encode(this.exception), 600);
259
260             var img = [];
261             for (var i = 0; i < chunks.length; i++) {
262                 var part = i + 1 + '/' + chunks.length;
263                 var data = {data: this.base64encode('hash=' + hash + '&part=' + part + '&data=' + chunks[i])};
264
265                 var url = baseUrl + '?' + Ext.urlEncode(data);
266                 img.push(Ext.DomHelper.insertFirst(this.el, {tag: 'img', src: url, hidden: true}, true));
267             }
268         }
269
270         if (! this.nonInteractive) {
271             window.setTimeout(this.showTransmissionCompleted, 4000);
272         }
273         
274         this.close();
275     },
276     
277     /**
278      * @private
279      */
280     showTransmissionCompleted: function () {
281         Ext.MessageBox.show({
282             title: _('Transmission Completed'),
283             msg: _('Your report has been sent. Thanks for your contribution') + '<br /><b>' + _('Please restart your browser now!') + '</b>',
284             buttons: Ext.MessageBox.OK,
285             icon: Ext.MessageBox.INFO
286         });
287     },
288     
289     /**
290      * @private
291      */
292     strChunk: function (str, chunklen) {
293         var chunks = [];
294         
295         var numChunks = Math.ceil(str.length / chunklen);
296         for (var i = 0; i < str.length; i +=  chunklen) {
297             chunks.push(str.substr(i, chunklen));
298         }
299         return chunks;
300     },
301     
302     /**
303      * @private
304      */
305     generateHash: function () {
306         // if the time isn't unique enough, the addition 
307         // of random chars should be
308         var t = String(new Date().getTime()).substr(4);
309         var s = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
310         for(var i = 0; i < 4; i++){
311             t += s.charAt(Math.floor(Math.random()*26));
312         }
313         return t;
314     },
315     
316
317     /**
318      * base 64 encode given string
319      * @private
320      */
321     base64encode : function (input) {
322         var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
323         var output = "";
324         var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
325         var i = 0;
326
327         while (i < input.length) {
328
329             chr1 = input.charCodeAt(i++);
330             chr2 = input.charCodeAt(i++);
331             chr3 = input.charCodeAt(i++);
332
333             enc1 = chr1 >> 2;
334             enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
335             enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
336             enc4 = chr3 & 63;
337
338             if (isNaN(chr2)) {
339                 enc3 = enc4 = 64;
340             } else if (isNaN(chr3)) {
341                 enc4 = 64;
342             }
343
344             output = output +
345             keyStr.charAt(enc1) + keyStr.charAt(enc2) +
346             keyStr.charAt(enc3) + keyStr.charAt(enc4);
347
348         }
349
350         return output;
351     }
352 });