1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 
 25 /**
 26  * Creates a new debug window. The document inside is not kept open.  All the
 27  * output goes into a single <div> element.
 28  * @constructor
 29  * @class
 30  * This class pops up a debug window and provides functions to send output there
 31  * in various ways. The output is continuously appended to the bottom of the
 32  * window. The document is left unopened so that the browser doesn't think it's
 33  * continuously loading and keep its little icon flailing forever. Also, the DOM
 34  * tree can't be manipulated on an open document. All the output is added to the
 35  * window by appending it the DOM tree. Another method of appending output is to
 36  * open the document and use document.write(), but then the document is left open.
 37  * <p>
 38  * Any client that uses this class can turn off debugging by changing the first
 39  * argument to the constructor to {@link AjxDebug.NONE}.
 40  *
 41  * @author Conrad Damon
 42  * @author Ross Dargahi
 43  *
 44  * @param {constant}	level	 	debug level for the current debugger (no window will be displayed for a level of NONE)
 45  * @param {string}		name 		the name of the window (deprecated)
 46  * @param {boolean}		showTime	if <code>true</code>, display timestamps before debug messages
 47  * @param {constant}	target		output target (AjxDebug.TGT_WINDOW | AjxDebug.TGT_CONSOLE)
 48  *
 49  * @private
 50  */
 51 AjxDebug = function(params) {
 52 
 53 	if (arguments.length == 0) {
 54 		params = {};
 55 	}
 56 	else if (typeof arguments[0] == "number") {
 57 		params = {level:arguments[0], name:arguments[1], showTime:arguments[2]};
 58 	}
 59 
 60 	this._showTime = params.showTime;
 61 	this._target = params.target || AjxDebug.TGT_WINDOW;
 62 	this._showTiming = false;
 63 	this._startTimePt = this._lastTimePt = 0;
 64 	this._dbgWindowInited = false;
 65 
 66 	this._msgQueue = [];
 67 	this._isPrevWinOpen = false;
 68 	this.setDebugLevel(params.level);
 69 };
 70 
 71 AjxDebug.prototype.toString = function() { return "AjxDebug"; };
 72 AjxDebug.prototype.isAjxDebug = true;
 73 
 74 /**
 75  * Defines "no debugging" level.
 76  */
 77 AjxDebug.NONE = 0; // no debugging (window will not come up)
 78 /**
 79  * Defines "minimal" debugging level.
 80  */
 81 AjxDebug.DBG1 = 1; // minimal debugging
 82 /**
 83  * Defines "moderate" debugging level.
 84  */
 85 AjxDebug.DBG2 = 2; // moderate debugging
 86 /**
 87  * Defines "all" debugging level.
 88  */
 89 AjxDebug.DBG3 = 3; // anything goes
 90 
 91 // log output targets
 92 AjxDebug.TGT_WINDOW		= "window";
 93 AjxDebug.TGT_CONSOLE	= "console";
 94 
 95 // holds log output in memory so we can show it to user if requested; hash of arrays by type
 96 AjxDebug.BUFFER		= {};
 97 AjxDebug.BUFFER_MAX	= {};
 98 
 99 // Special log types. These can be used to make high-priority log info available in prod mode.
100 // To turn off logging for a type, set its BUFFER_MAX to 0.
101 AjxDebug.DEFAULT_TYPE	= "debug";		// regular DBG messages
102 AjxDebug.RPC			= "rpc";		// for troubleshooting "Out of RPC cache" errors
103 AjxDebug.NOTIFY			= "notify";		// for troubleshooting missing new mail
104 AjxDebug.EXCEPTION		= "exception";	// JS errors
105 AjxDebug.CALENDAR		= "calendar";	// for troubleshooting calendar errors
106 AjxDebug.REPLY			= "reply";		// bug 56308
107 AjxDebug.SCROLL			= "scroll"; 	// bug 55775
108 AjxDebug.BAD_JSON		= "bad_json"; 	// bug 57066
109 AjxDebug.PREFS			= "prefs";		// bug 60942
110 AjxDebug.PROGRESS       = "progress";	// progress dialog
111 AjxDebug.REMINDER       = "reminder";   // bug 60692
112 AjxDebug.OFFLINE        = "offline";
113 AjxDebug.TAG_ICON       = "tagIcon";    // bug 62155
114 AjxDebug.DATA_URI       = "dataUri";    // bug 64693
115 AjxDebug.MSG_DISPLAY	= "msgDisplay";	// bugs 68599, 69616
116 AjxDebug.ZIMLET			= "zimlet";		// bugs 83009
117 AjxDebug.KEYBOARD		= "kbnav";		// keyboard manager debugging
118 AjxDebug.FOCUS			= "focus";		// focus
119 AjxDebug.FOCUS1			= "focus1";		// focus - minimal logging
120 AjxDebug.ACCESSIBILITY	= "a11y";		// accessibility logging
121 AjxDebug.DRAFT	        = "draft";		// draft auto-save
122 
123 AjxDebug.BUFFER_MAX[AjxDebug.DEFAULT_TYPE]	= 0;	// this one can get big due to object dumps
124 AjxDebug.BUFFER_MAX[AjxDebug.RPC]			= 200;
125 AjxDebug.BUFFER_MAX[AjxDebug.NOTIFY]		= 400;
126 AjxDebug.BUFFER_MAX[AjxDebug.EXCEPTION]		= 100;
127 AjxDebug.BUFFER_MAX[AjxDebug.CALENDAR]		= 400;
128 AjxDebug.BUFFER_MAX[AjxDebug.REPLY]			= 400;
129 AjxDebug.BUFFER_MAX[AjxDebug.SCROLL]		= 100;
130 AjxDebug.BUFFER_MAX[AjxDebug.BAD_JSON]		= 200;
131 AjxDebug.BUFFER_MAX[AjxDebug.PREFS] 		= 200;
132 AjxDebug.BUFFER_MAX[AjxDebug.REMINDER]		= 200;
133 AjxDebug.BUFFER_MAX[AjxDebug.OFFLINE]		= 400;
134 AjxDebug.BUFFER_MAX[AjxDebug.TAG_ICON]		= 200;
135 AjxDebug.BUFFER_MAX[AjxDebug.PROGRESS]		= 200;
136 AjxDebug.BUFFER_MAX[AjxDebug.DATA_URI]		= 200;
137 AjxDebug.BUFFER_MAX[AjxDebug.MSG_DISPLAY]	= 200;
138 AjxDebug.BUFFER_MAX[AjxDebug.ZIMLET]		= 200;
139 AjxDebug.BUFFER_MAX[AjxDebug.KEYBOARD]		= null;
140 AjxDebug.BUFFER_MAX[AjxDebug.FOCUS]			= null;
141 AjxDebug.BUFFER_MAX[AjxDebug.ACCESSIBILITY]	= null;
142 AjxDebug.BUFFER_MAX[AjxDebug.DRAFT]	        = 200;
143 
144 AjxDebug.MAX_OUT = 25000; // max length capable of outputting an XML msg
145 
146 AjxDebug._CONTENT_FRAME_ID	= "AjxDebug_CF";
147 AjxDebug._LINK_FRAME_ID		= "AjxDebug_LF";
148 AjxDebug._BOTTOM_FRAME_ID	= "AjxDebug_BFI";
149 AjxDebug._BOTTOM_FRAME_NAME	= "AjxDebug_BFN";
150 
151 AjxDebug.prototype.setTitle =
152 function(title) {
153 	if (this._document && !AjxEnv.isIE) {
154 		this._document.title = title;
155 	}
156 };
157 
158 /**
159  * Set debug level. May open or close the debug window if moving to or from level {@link AjxDebug.NONE}.
160  *
161  * @param {constant}	level	 	debug level for the current debugger
162  */
163 AjxDebug.prototype.setDebugLevel =
164 function(level) {
165 
166 	this._level = parseInt(level) || level;
167 	this._enable(this._level != AjxDebug.NONE);
168 };
169 
170 /**
171  * Gets the current debug level.
172  * 
173  * @return	{constant}	the debug level
174  */
175 AjxDebug.prototype.getDebugLevel =
176 function() {
177 	return this._level;
178 };
179 
180 /**
181  * Prints a debug message. Any HTML will be rendered, and a line break is added.
182  *
183  * @param {constant}	level	 	debug level for the current debugger
184  * @param {string}	msg		the text to display
185  */
186 AjxDebug.prototype.println =
187 function(level, msg, linkName) {
188 	
189 	if (!this._isWriteable()) { return; }
190 
191 	try {
192 		var result = this._handleArgs(arguments);
193 		if (!result) { return; }
194 
195 		msg = result.args.join("");
196 		var eol = (this._target != AjxDebug.TGT_CONSOLE) ? "<br>" : "";
197 		this._add({msg:this._timestamp() + msg + eol, linkName:result.linkName, level:level});
198 	} catch (ex) {
199 		// do nothing
200 	}
201 };
202 
203 /**
204  * Checks if debugging is disabled.
205  * 
206  * @return	{boolean}		<code>true</code> if disabled
207  */
208 AjxDebug.prototype.isDisabled =
209 function () {
210 	return !this._enabled;
211 };
212 
213 /**
214  * Prints an object into a table, with a column for properties and a column for values. Above the table is a header with the object
215  * class and the CSS class (if any). The properties are sorted (numerically if they're all numbers). Creating and appending table
216  * elements worked in Mozilla but not IE. Using the insert* methods works for both. Properties that are function
217  * definitions are skipped.
218  *
219  * @param {constant}	level	 	debug level for the current debugger
220  * @param {object}	obj		the object to be printed
221  * @param {boolean}	showFuncs		if <code>true</code>, show props that are functions
222  */
223 AjxDebug.prototype.dumpObj =
224 function(level, obj, showFuncs, linkName) {
225 	if (!this._isWriteable()) { return; }
226 
227 	var result = this._handleArgs(arguments);
228 	if (!result) { return; }
229 
230 	obj = result.args[0];
231 	if (!obj) { return; }
232 
233 	showFuncs = result.args[1];
234 	this._add({obj:obj, linkName:result.linkName, showFuncs:showFuncs, level:level});
235 };
236 
237 /**
238  * Dumps a bunch of text into a <textarea>, so that it is wrapped and scrollable. HTML will not be rendered.
239  *
240  * @param {constant}	level	 	debug level for the current debugger
241  * @param {string}	text		the text to output as is
242  */
243 AjxDebug.prototype.printRaw =
244 function(level, text, linkName) {
245 	if (!this._isWriteable()) { return; }
246 
247 	var result = this._handleArgs(arguments);
248 	if (!result) { return; }
249 
250 	this._add({obj:result.args[0], isRaw:true, linkName:result.linkName, level:level});
251 };
252 
253 /**
254  * Pretty-prints a chunk of XML, doing color highlighting for different types of nodes.
255  *
256  * @param {constant}	level	 	debug level for the current debugger
257  * @param {string}	text		some XML
258  * 
259  * TODO: fix for printing to console
260  */
261 AjxDebug.prototype.printXML =
262 function(level, text, linkName) {
263 	if (!this._isWriteable()) { return; }
264 
265 	var result = this._handleArgs(arguments);
266 	if (!result) { return; }
267 
268 	text = result.args[0];
269 	if (!text) { return; }
270 
271 	// skip generating pretty xml if theres too much data
272 	if (text.length > AjxDebug.MAX_OUT) {
273 		this.printRaw(text);
274 		return;
275 	}
276 	this._add({obj:text, isXml:true, linkName:result.linkName, level:level});
277 };
278 
279 /**
280  * Reveals white space in text by replacing it with tags.
281  *
282  * @param {constant}	level	 	debug level for the current debugger
283  * @param {string}	text		the text to be displayed
284  */
285 AjxDebug.prototype.display =
286 function(level, text, linkName) {
287 	if (!this._isWriteable()) { return; }
288 
289 	var result = this._handleArgs(arguments);
290 	if (!result) { return; }
291 
292 	text = result.args[0];
293 	text = text.replace(/\r?\n/g, '[crlf]');
294 	text = text.replace(/ /g, '[space]');
295 	text = text.replace(/\t/g, '[tab]');
296 	this.printRaw(level, text, linkName);
297 };
298 
299 /**
300  * Turn the display of timing statements on/off.
301  *
302  * @param {boolean}	on			if <code>true</code>, display timing statements
303  * @param {string}	msg		the message to show when timing is turned on
304  */
305 AjxDebug.prototype.showTiming =
306 function(on, msg) {
307 	this._showTiming = on;
308 	if (on) {
309 		this._enable(true);
310 	}
311 	var state = on ? "on" : "off";
312 	var text = "Turning timing info " + state;
313 	if (msg) {
314 		text = text + ": " + msg;
315 	}
316 
317 	var debugMsg = new DebugMessage({msg:text});
318 	this._addMessage(debugMsg);
319 	this._startTimePt = this._lastTimePt = new Date().getTime();
320 };
321 
322 /**
323  * Displays time elapsed since last time point.
324  *
325  * @param {string}	msg		the text to display with timing info
326  * @param {boolean}	restart	if <code>true</code>, set timer back to zero
327  */
328 AjxDebug.prototype.timePt =
329 function(msg, restart) {
330 	if (!this._showTiming || !this._isWriteable()) { return; }
331 
332 	if (restart) {
333 		this._startTimePt = this._lastTimePt = new Date().getTime();
334 	}
335 	var now = new Date().getTime();
336 	var elapsed = now - this._startTimePt;
337 	var interval = now - this._lastTimePt;
338 	this._lastTimePt = now;
339 
340 	var spacer = restart ? "<br/>" : "";
341 	msg = msg ? " " + msg : "";
342 	var text = [spacer, "[", elapsed, " / ", interval, "]", msg].join("");
343 	var html = "<div>" + text + "</div>";
344 
345 	// Add the message to our stack
346 	this._addMessage(new DebugMessage({msg:html}));
347 	return interval;
348 };
349 
350 AjxDebug.prototype.getContentFrame =
351 function() {
352 	if (this._contentFrame) {
353 		return this._contentFrame;
354 	}
355 	if (this._debugWindow && this._debugWindow.document) {
356 		return this._debugWindow.document.getElementById(AjxDebug._CONTENT_FRAME_ID);
357 	}
358 	return null;
359 };
360 
361 AjxDebug.prototype.getLinkFrame =
362 function(noOpen) {
363 	if (this._linkFrame) {
364 		return this._linkFrame;
365 	}
366 	if (this._debugWindow && this._debugWindow.document) {
367 		return this._debugWindow.document.getElementById(AjxDebug._LINK_FRAME_ID);
368 	}
369 	if (!noOpen) {
370 		this._openDebugWindow();
371 		return this.getLinkFrame(true);
372 	}
373 	return null;
374 };
375 
376 // Private methods
377 
378 AjxDebug.prototype._enable =
379 function(enabled) {
380 
381 	this._enabled = enabled;
382 	if (this._target == AjxDebug.TGT_WINDOW) {
383 		if (enabled) {
384 			if (!this._dbgName) {
385 				this._dbgName = "AjxDebugWin_" + location.hostname.replace(/\./g,'_');
386 			}
387 			if (this._debugWindow == null || this._debugWindow.closed) {
388 				this._openDebugWindow();
389 			}
390 		} else {
391 			if (this._debugWindow) {
392 				this._debugWindow.close();
393 				this._debugWindow = null;
394 			}
395 		}
396 	}
397 };
398 
399 AjxDebug.prototype._isWriteable =
400 function() {
401 	if (this.isDisabled()) {
402 		return false;
403 	}
404 	if (this._target == AjxDebug.TGT_WINDOW) {
405 		try {
406 			return (!this._isPaused && this._debugWindow && !this._debugWindow.closed);
407 		} catch (ex) {
408 			// OMG accessing the debugWindow in IE12 is sometimes throwing a COM exception 'An outgoing call
409 			// cannot be made since the application is dispatching an input-synchronous call'.  IOleWindow.GetWindow
410 			// is marked with input-sync and the exception implies an attempt to access another COM apartment while
411 			// executing it.  Sounds like an IE12 Bug with this preview version.
412 
413 			// Just suppress the logging for this call, since it appears _debugWindow is inaccessible
414 			return false;
415 
416 		}
417 	}
418 	return true;
419 };
420 
421 AjxDebug.prototype._getHtmlForObject =
422 function(obj, params) {
423 
424 	params = params || {};
425 	var html = [];
426 	var idx = 0;
427 
428 	if (obj === undefined) {
429 		html[idx++] = "<span>Undefined</span>";
430 	} else if (obj === null) {
431 		html[idx++] = "<span>NULL</span>";
432 	} else if (AjxUtil.isBoolean(obj)) {
433 		html[idx++] = "<span>" + obj + "</span>";
434 	} else if (AjxUtil.isNumber(obj)) {
435 		html[idx++] = "<span>" + obj +"</span>";
436 	} else {
437 		if (params.isRaw) {
438 			html[idx++] = this._timestamp();
439 			html[idx++] = "<textarea rows='25' style='width:100%' readonly='true'>";
440 			html[idx++] = obj;
441 			html[idx++] = "</textarea><p></p>";
442 		} else if (params.isXml) {
443 			var xmldoc = new AjxDebugXmlDocument;
444 			var doc = xmldoc.create();
445 			// IE bizarrely throws error if we use doc.loadXML here (bug 40451)
446 			if (doc && ("loadXML" in doc)) {
447 				doc.loadXML(obj);
448 				html[idx++] = "<div style='border-width:2px; border-style:inset; width:100%; height:300px; overflow:auto'>";
449 				html[idx++] = this._createXmlTree(doc, 0, {"authToken":true});
450 				html[idx++] = "</div>";
451 			} else {
452 				html[idx++] = "<span>Unable to create XmlDocument to show XML</span>";
453 			}
454 		} else {
455 			html[idx++] = "<div style='border-width:2px; border-style:inset; width:100%; height:300px; overflow:auto'><pre>";
456 			html[idx++] = this._dump(obj, true, params.showFuncs, {"ZmAppCtxt":true, "authToken":true});
457 			html[idx++] = "</div></pre>";
458 		}
459 	}
460 	return html.join("");
461 };
462 
463 // Pretty-prints a Javascript object
464 AjxDebug.prototype._dump =
465 function(obj, recurse, showFuncs, omit) {
466 
467 	return AjxStringUtil.prettyPrint(obj, recurse, showFuncs, omit);
468 };
469 
470 /**
471  * Marshals args to public debug functions. In general, the debug level is an optional
472  * first arg. If the first arg is a debug level, check it and then strip it from the args.
473  * The last argument is an optional name for the link from the left panel.
474  *
475  * Returns an object with the link name and a list of the arguments (other than level and
476  * link name).
477  *
478  * @param {array}	args				an arguments list
479  *
480  * @private
481  */
482 AjxDebug.prototype._handleArgs =
483 function(args) {
484 
485 	// don't output anything if debugging is off, or timing is on
486 	if (this._level == AjxDebug.NONE || this._showTiming || args.length == 0) { return; }
487 
488 	// convert args to a true Array so they're easier to deal with
489 	var argsArray = new Array(args.length);
490 	for (var i = 0; i < args.length; i++) {
491 		argsArray[i] = args[i];
492 	}
493 
494 	var result = {args:null, linkName:null};
495 
496 	// remove link name from arg list if present - check if last arg is *Request or *Response
497 	var origLen = argsArray.length;
498 	if (argsArray.length > 1) {
499 		var lastArg = argsArray[argsArray.length - 1];
500 		if (lastArg && lastArg.indexOf && (lastArg.indexOf("DebugWarn") != -1 || ((lastArg.indexOf(" ") == -1) && (/Request|Response$/.test(lastArg))))) {
501 			result.linkName = lastArg;
502 			argsArray.pop();
503 		}
504 	}
505 
506 	// check level if provided, strip it from args; level is either a number, or 1-8 lowercase letters/numbers
507 	var userLevel = null;
508 	var firstArg = argsArray[0];
509 	var gotUserLevel = (typeof firstArg == "number" || ((origLen > 1) && firstArg.length <= 8 && /^[a-z0-9]+$/.test(firstArg)));
510 	if (gotUserLevel) {
511 		userLevel = firstArg;
512 		argsArray.shift();
513 	}
514 	if (userLevel && (AjxDebug.BUFFER_MAX[userLevel] == null)) {
515 		if (typeof this._level == "number") {
516 			if (typeof userLevel != "number" || (userLevel > this._level)) { return; }
517 		} else {
518 			if (userLevel != this._level) { return; }
519 		}
520 	}
521 	result.args = argsArray;
522 
523 	return result;
524 };
525 
526 AjxDebug.prototype._openDebugWindow =
527 function(force) {
528 	var name = AjxEnv.isIE ? "_blank" : this._dbgName;
529 	this._debugWindow = window.open("", name, "width=600,height=400,resizable=yes,scrollbars=yes");
530 
531 	if (this._debugWindow == null) {
532 		this._enabled = false;
533 		return;
534 	}
535 
536 	this._enabled = true;
537 	this._isPrevWinOpen = this._debugWindow.debug;
538 	this._debugWindow.debug = true;
539 
540 	try {
541 		this._document = this._debugWindow.document;
542 		this.setTitle("Debug");
543 
544 		if (!this._isPrevWinOpen) {
545 			this._document.write(
546 				"<html>",
547 					"<head>",
548 						"<script>",
549 							"function blank() {return [",
550 								"'<html><head><style type=\"text/css\">',",
551 									"'P, TD, DIV, SPAN, SELECT, INPUT, TEXTAREA, BUTTON {',",
552 											"'font-family: Tahoma, Arial, Helvetica, sans-serif;',",
553 											"'font-size:11px;}',",
554 									"'.Content {display:block;margin:0.25em 0em;}',",
555 									"'.Link {cursor: pointer;color:blue;text-decoration:underline;white-space:nowrap;width:100%;}',",
556 									"'.DebugWarn {color:red;font-weight:bold;}',",
557 									"'.Run {color:black; background-color:red;width:100%;font-size:18px;font-weight:bold;}',",
558 									"'.RunLink {display:block;color:black;background-color:red;font-weight:bold;white-space:nowrap;width:100%;}',",
559 								"'</style></head><body></body></html>'].join(\"\");}",
560 						"</script>",
561 					"</head>",
562 					"<frameset cols='125, *'>",
563 						"<frameset rows='*,40'>",
564 							"<frame name='", AjxDebug._LINK_FRAME_ID, "' id='", AjxDebug._LINK_FRAME_ID, "' src='javascript:parent.parent.blank();'>",
565 							"<frame name='", AjxDebug._BOTTOM_FRAME_NAME, "' id='", AjxDebug._BOTTOM_FRAME_ID, "' src='javascript:parent.parent.blank();' scrolling=no frameborder=0>",
566 						"</frameset>",
567 						"<frame name='", AjxDebug._CONTENT_FRAME_ID, "' id='", AjxDebug._CONTENT_FRAME_ID, "' src='javascript:parent.blank();'>",
568 					"</frameset>",
569 				"</html>"
570 			);
571 			this._document.close();
572 			
573 			var ta = new AjxTimedAction(this, AjxDebug.prototype._finishInitWindow);
574 			AjxTimedAction.scheduleAction(ta, 2500);
575 		} else {
576 			this._finishInitWindow();
577 
578 			this._contentFrame = this._document.getElementById(AjxDebug._CONTENT_FRAME_ID);
579 			this._linkFrame = this._document.getElementById(AjxDebug._LINK_FRAME_ID);
580 			this._createLinkNContent("RunLink", "NEW RUN", "Run", "NEW RUN");
581 
582 			this._attachHandlers();
583 
584 			this._dbgWindowInited = true;
585 			// show any messages that have been queued up, while the window loaded.
586 			this._showMessages();
587 		}
588 	} catch (ex) {
589 		if (this._debugWindow) {
590 			this._debugWindow.close();
591 		}
592 		this._openDebugWindow(true);
593 	}
594 };
595 
596 AjxDebug.prototype._finishInitWindow =
597 function() {
598 	try {
599 		this._contentFrame = this._debugWindow.document.getElementById(AjxDebug._CONTENT_FRAME_ID);
600 		this._linkFrame = this._debugWindow.document.getElementById(AjxDebug._LINK_FRAME_ID);
601 
602 		var frame = this._debugWindow.document.getElementById(AjxDebug._BOTTOM_FRAME_ID);
603 		var doc = frame.contentWindow.document;
604 		var html = [];
605 		var i = 0;
606 		html[i++] = "<table><tr><td><button id='";
607 		html[i++] = AjxDebug._BOTTOM_FRAME_ID;
608 		html[i++] = "_clear'>Clear</button></td><td><button id='";
609 		html[i++] = AjxDebug._BOTTOM_FRAME_ID;
610 		html[i++] = "_pause'>Pause</button></td></tr></table>";
611 		if (doc.body) {
612 			doc.body.innerHTML = html.join("");
613 		}
614 	}
615 	catch (ex) {
616 		// IE chokes on the popup window on cold start-up (when IE is started
617 		// for the first time after system reboot). This should not prevent the
618 		// app from running and should not bother the user
619 	}
620 
621 	if (doc) {
622 		this._clearBtn = doc.getElementById(AjxDebug._BOTTOM_FRAME_ID + "_clear");
623 		this._pauseBtn = doc.getElementById(AjxDebug._BOTTOM_FRAME_ID + "_pause");
624 	}
625 
626 	this._attachHandlers();
627 	this._dbgWindowInited = true;
628 	this._showMessages();
629 };
630 
631 AjxDebug.prototype._attachHandlers =
632 function() {
633 	// Firefox allows us to attach an event listener, and runs it even though
634 	// the window with the code is gone ... odd, but nice. IE, though will not
635 	// run the handler, so we make sure, even if we're  coming back to the
636 	// window, to attach the onunload handler. In general reattach all handlers
637 	// for IE
638 	var unloadHandler = AjxCallback.simpleClosure(this._unloadHandler, this);
639 	if (this._debugWindow.attachEvent) {
640 		this._unloadHandler = unloadHandler;
641 		this._debugWindow.attachEvent('onunload', unloadHandler);
642 	}
643 	else {
644 		this._debugWindow.onunload = unloadHandler;
645 	}
646 
647 	if (this._clearBtn) {
648 		this._clearBtn.onclick = AjxCallback.simpleClosure(this._clear, this);
649 	}
650 	if (this._pauseBtn) {
651 		this._pauseBtn.onclick = AjxCallback.simpleClosure(this._pause, this);
652 	}
653 };
654 
655 /**
656  * Scrolls to the bottom of the window. How it does that depends on the browser.
657  *
658  * @private
659  */
660 AjxDebug.prototype._scrollToBottom =
661 function() {
662 	var contentFrame = this.getContentFrame();
663 	var contentBody = contentFrame ? contentFrame.contentWindow.document.body : null;
664 	var linkFrame = this.getLinkFrame();
665 	var linkBody = linkFrame ? linkFrame.contentWindow.document.body : null;
666 
667 	if (contentBody && linkBody) {
668 		contentBody.scrollTop = contentBody.scrollHeight;
669 		linkBody.scrollTop = linkBody.scrollHeight;
670 	}
671 };
672 
673 /**
674  * Returns a timestamp string, if we are showing them.
675  * @private
676  */
677 AjxDebug.prototype._timestamp =
678 function() {
679 	return this._showTime ? this._getTimeStamp() + ": " : "";
680 };
681 
682 AjxDebug.prototype.setShowTimestamps =
683 function(show) {
684 	this._showTime = show;
685 };
686 
687 /**
688  * This function takes an XML node and returns an HTML string that displays that node
689  * the indent argument is used to describe what depth the node is at so that
690  * the HTML code can create a nice indentation.
691  * 
692  * @private
693  */
694 AjxDebug.prototype._createXmlTree =
695 function (node, indent, omit) {
696 	if (node == null) { return ""; }
697 
698 	var str = "";
699 	var len;
700 	switch (node.nodeType) {
701 		case 1:	// Element
702 			str += "<div style='color: blue; padding-left: 16px;'><<span style='color: DarkRed;'>" + node.nodeName + "</span>";
703 
704 			if (omit && omit[node.nodeName]) {
705 				return str + "/></div>";
706 			}
707 
708 			var attrs = node.attributes;
709 			len = attrs.length;
710 			for (var i = 0; i < len; i++) {
711 				str += this._createXmlAttribute(attrs[i]);
712 			}
713 
714 			if (!node.hasChildNodes()) {
715 				return str + "/></div>";
716 			}
717 			str += "><br />";
718 
719 			var cs = node.childNodes;
720 			len = cs.length;
721 			for (var i = 0; i < len; i++) {
722 				str += this._createXmlTree(cs[i], indent + 3, omit);
723 			}
724 			str += "</<span style='color: DarkRed;'>" + node.nodeName + "</span>></div>";
725 			break;
726 
727 		case 9:	// Document
728 			var cs = node.childNodes;
729 			len = cs.length;
730 			for (var i = 0; i < len; i++) {
731 				str += this._createXmlTree(cs[i], indent, omit);
732 			}
733 			break;
734 
735 		case 3:	// Text
736 			if (!/^\s*$/.test(node.nodeValue)) {
737 				var val = node.nodeValue.replace(/</g, "<").replace(/>/g, ">");
738 				str += "<span style='color: WindowText; padding-left: 16px;'>" + val + "</span><br />";
739 			}
740 			break;
741 
742 		case 7:	// ProcessInstruction
743 			str += "<?" + node.nodeName;
744 
745 			var attrs = node.attributes;
746 			len = attrs.length;
747 			for (var i = 0; i < len; i++) {
748 				str += this._createXmlAttribute(attrs[i]);
749 			}
750 			str+= "?><br />"
751 			break;
752 
753 		case 4:	// CDATA
754 			str = "<div style=''><![CDATA[<span style='color: WindowText; font-family: \"Courier New\"; white-space: pre; display: block; border-left: 1px solid Gray; padding-left: 16px;'>" +
755 				  node.nodeValue +
756 				  "</span>]" + "]></div>";
757 			break;
758 
759 		case 8:	// Comment
760 			str = "<div style='color: blue; padding-left: 16px;'><!--<span style='white-space: pre; font-family: \"Courier New\"; color: Gray; display: block;'>" +
761 				  node.nodeValue +
762 				  "</span>--></div>";
763 			break;
764 
765 		case 10:
766 				str = "<div style='color: blue; padding-left: 16px'><!DOCTYPE " + node.name;
767 				if (node.publicId) {
768 					str += " PUBLIC \"" + node.publicId + "\"";
769 					if (node.systemId)
770 						str += " \"" + node.systemId + "\"";
771 				}
772 				else if (node.systemId) {
773 					str += " SYSTEM \"" + node.systemId + "\"";
774 				}
775 				str += "></div>";
776 
777 				// TODO: Handle custom DOCTYPE declarations (ELEMENT, ATTRIBUTE, ENTITY)
778 				break;
779 
780 		default:
781 			this._inspect(node);
782 	}
783 
784 	return str;
785 };
786 
787 AjxDebug.prototype._createXmlAttribute =
788 function(a) {
789 	return [" <span style='color: red'>", a.nodeName, "</span><span style='color: blue'>=\"", a.nodeValue, "\"</span>"].join("");
790 };
791 
792 AjxDebug.prototype._inspect =
793 function(obj) {
794 	var str = "";
795 	for (var k in obj) {
796 		str += "obj." + k + " = " + obj[k] + "\n";
797 	}
798 	window.alert(str);
799 };
800 
801 AjxDebug.prototype._add =
802 function(params) {
803 
804 	params.extraHtml = params.obj && this._getHtmlForObject(params.obj, params);
805 
806 	// Add the message to our stack
807     this._addMessage(new DebugMessage(params));
808 };
809 
810 AjxDebug.prototype._addMessage =
811 function(msg) {
812 	this._msgQueue.push(msg);
813 	this._showMessages();
814 };
815 
816 AjxDebug.prototype._showMessages =
817 function() {
818 
819 	switch (this._target) {
820 		case AjxDebug.TGT_WINDOW:
821 			this._showMessagesInWindow();
822 			break;
823 		case AjxDebug.TGT_CONSOLE:
824 			this._showMessagesInConsole();
825 	}
826 	this._addMessagesToBuffer();
827 	this._msgQueue = [];
828 };
829 
830 AjxDebug.prototype._showMessagesInWindow =
831 function() {
832 
833 	if (!this._dbgWindowInited) {
834 		// For now, don't show the messages-- assuming that this case only
835 		// happens at startup, and many messages will be written
836 		return;
837 	}
838 	try {
839 		if (this._msgQueue.length > 0) {
840 			var contentFrame = this.getContentFrame();
841 			var linkFrame = this.getLinkFrame();
842 			if (!contentFrame || !linkFrame) { return; }
843 
844 			var contentFrameDoc = contentFrame.contentWindow.document;
845 			var linkFrameDoc = linkFrame.contentWindow.document;
846 			var now = new Date();
847 			for (var i = 0, len = this._msgQueue.length; i < len; ++i ) {
848 				var msg = this._msgQueue[i];
849 				var linkLabel = msg.linkName;
850 				var contentLabel = [msg.message, msg.extraHtml].join("");
851 				this._createLinkNContent("Link", linkLabel, "Content", contentLabel, now);
852 			}
853 		}
854 
855 		this._scrollToBottom();
856 	} catch (ex) {}
857 };
858 
859 AjxDebug.prototype._addMessagesToBuffer =
860 function() {
861 
862 	var eol = (this._target == AjxDebug.TGT_CONSOLE) ? "<br>" : "";
863 	for (var i = 0, len = this._msgQueue.length; i < len; ++i ) {
864 		var msg = this._msgQueue[i];
865 		AjxDebug._addMessageToBuffer(msg.type, msg.message + msg.extraHtml + eol);
866 	}
867 };
868 
869 AjxDebug._addMessageToBuffer =
870 function(type, msg) {
871 
872 	type = type || AjxDebug.DEFAULT_TYPE;
873 	var max = AjxDebug.BUFFER_MAX[type];
874 	if (max > 0) {
875 		var buffer = AjxDebug.BUFFER[type] = AjxDebug.BUFFER[type] || [];
876 		while (buffer.length >= max) {
877 			buffer.shift();
878 		}
879 		buffer.push(msg);
880 	}
881 };
882 
883 AjxDebug.prototype._showMessagesInConsole =
884 function() {
885 
886 	if (!window.console) { return; }
887 
888 	var now = new Date();
889 	for (var i = 0, len = this._msgQueue.length; i < len; ++i ) {
890 		var msg = this._msgQueue[i];
891 		if (window.console && window.console.log) {
892 			window.console.log(AjxStringUtil.stripTags(msg.message + msg.extraHtml));
893 		}
894 	}
895 };
896 
897 AjxDebug.prototype._getTimeStamp =
898 function(date) {
899 	if (!AjxDebug._timestampFormatter) {
900 		AjxDebug._timestampFormatter = new AjxDateFormat("HH:mm:ss.SSS");
901 	}
902 	date = date || new Date();
903 	return AjxStringUtil.htmlEncode(AjxDebug._timestampFormatter.format(date), true);
904 };
905 
906 AjxDebug.prototype._createLinkNContent =
907 function(linkClass, linkLabel, contentClass, contentLabel, now) {
908 
909 	var linkFrame = this.getLinkFrame();
910 	if (!linkFrame) { return; }
911 
912 	now = now || new Date();
913 	var timeStamp = ["[", this._getTimeStamp(now), "]"].join("");
914 	var id = "Lnk_" + now.getTime();
915 
916 	// create link
917 	if (linkLabel) {
918 		var linkFrameDoc = linkFrame.contentWindow.document;
919 		var linkEl = linkFrameDoc.createElement("DIV");
920 		linkEl.className = linkClass;
921 		linkEl.innerHTML = [linkLabel, timeStamp].join(" - ");
922 		linkEl._targetId = id;
923 		linkEl._dbg = this;
924 		linkEl.onclick = AjxDebug._linkClicked;
925 
926 		var linkBody = linkFrameDoc.body;
927 		linkBody.appendChild(linkEl);
928 	}
929 
930 	// create content
931 	var contentFrameDoc = this.getContentFrame().contentWindow.document;
932 	var contentEl = contentFrameDoc.createElement("DIV");
933 	contentEl.className = contentClass;
934 	contentEl.id = id;
935 	contentEl.innerHTML = contentLabel;
936 
937 	contentFrameDoc.body.appendChild(contentEl);
938 
939 	// always show latest
940 	this._scrollToBottom();
941 };
942 
943 AjxDebug._linkClicked =
944 function() {
945 	var contentFrame = this._dbg.getContentFrame();
946 	var el = contentFrame.contentWindow.document.getElementById(this._targetId);
947 	var y = 0;
948 	while (el) {
949 		y += el.offsetTop;
950 		el = el.offsetParent;
951 	}
952 
953 	contentFrame.contentWindow.scrollTo(0, y);
954 };
955 
956 AjxDebug.prototype._clear =
957 function() {
958 	this.getContentFrame().contentWindow.document.body.innerHTML = "";
959 	this.getLinkFrame().contentWindow.document.body.innerHTML = "";
960 };
961 
962 AjxDebug.prototype._pause =
963 function() {
964 	this._isPaused = !this._isPaused;
965 	this._pauseBtn.innerHTML = this._isPaused ? "Resume" : "Pause";
966 };
967 
968 AjxDebug.prototype._unloadHandler =
969 function() {
970 	if (!this._debugWindow) { return; } // is there anything to do?
971 
972 	// detach event handlers
973 	if (this._debugWindow.detachEvent) {
974 		this._debugWindow.detachEvent('onunload', this._unloadHandler);
975 	} else {
976 		this._debugWindow.onunload = null;
977 	}
978 };
979 
980 AjxDebug.println =
981 function(type, msg) {
982 	AjxDebug._addMessageToBuffer(type, msg + "<br>");
983 };
984 
985 AjxDebug.dumpObj =
986 function(type, obj) {
987 	AjxDebug._addMessageToBuffer(type, "<pre>" + AjxStringUtil.prettyPrint(obj, true) + "</pre>");
988 };
989 
990 /**
991  *
992  * @param {hash}	params			hash of params:
993  * @param {string}	methodNameStr	SOAP method, eg SearchRequest or SearchResponse
994  * @param {boolean}	asyncMode		true if request made asynchronously
995  */
996 AjxDebug.logSoapMessage =
997 function(params) {
998 
999 	if (params.methodNameStr == "NoOpRequest" || params.methodNameStr == "NoOpResponse") { return; }
1000 
1001 	var ts = AjxDebug._getTimeStamp();
1002 	var msg = ["<b>", params.methodNameStr, params.asyncMode ? "" : " (SYNCHRONOUS)" , " - ", ts, "</b>"].join("");
1003 	for (var type in AjxDebug.BUFFER) {
1004 		if (type == AjxDebug.DEFAULT_TYPE) { continue; }
1005 		AjxDebug.println(type, msg);
1006 	}
1007 	if (window.DBG) {
1008         // Link is written here:
1009         var linkName = params.methodNameStr;
1010         if (!params.asyncMode) {
1011             linkName = "<span class='DebugWarn'>SYNCHRONOUS </span>" + linkName;
1012         }
1013         window.DBG.println(window.DBG._level, msg, linkName);
1014 	}
1015 };
1016 
1017 AjxDebug._getTimeStamp =
1018 function(date) {
1019 	return AjxDebug.prototype._getTimeStamp.apply(null, arguments);
1020 };
1021 
1022 AjxDebug.getDebugLog =
1023 function(type) {
1024 
1025 	type = type || AjxDebug.DEFAULT_TYPE;
1026 	var buffer = AjxDebug.BUFFER[type];
1027 	return buffer ? buffer.join("") : "";
1028 };
1029 
1030 /**
1031  * Simple wrapper for log messages.
1032  * @private
1033  */
1034 DebugMessage = function(params) {
1035 
1036 	params = params || {};
1037 	this.message = params.msg || "";
1038 	this.type = params.type || null;
1039 	this.category = params.category || "";
1040 	this.time = params.time || (new Date().getTime());
1041 	this.extraHtml = params.extraHtml || "";
1042 	this.linkName = params.linkName;
1043 	this.type = (params.level && typeof(params.level) == "string") ? params.level : AjxDebug.DEFAULT_TYPE;
1044 };
1045