1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2008, 2009, 2010, 2013, 2014, 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) 2008, 2009, 2010, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * 
 26  * @private
 27  */
 28 AjxLeakDetector = function() {
 29 	this._controls = [];
 30 	this._closures = {}; // Map of id to { closure, args }
 31 	this._closureReport = []; // Report that is created during dispose event, not actually reported till later.
 32 	this._nextId = 1;
 33 	this._addHooks();
 34 };
 35 
 36 /**
 37  * Executes a command. This is intended to be run by the client special search handler.
 38  * 
 39  * @param {string}	command 		"begin", "end", or "report"
 40  * @return {hash}	an object with 3 attributes: success, message, and details
 41  */
 42 AjxLeakDetector.execute =
 43 function(command) {
 44 	var result = {
 45 		success: false,
 46 		message: "",
 47 		details: ""
 48 	};
 49 	if (command == "begin") {
 50 		result.success = AjxLeakDetector.begin();
 51 		result.message = result.success ? "Leak detector started." : "Leak detector already started.";
 52 	} else if (command == "end") {
 53 		result.success = AjxLeakDetector.end();
 54 		result.message = result.success ? "Leak detector stopped." : "Leak detector is not running.";
 55 	} else if (command == "report" || command == "dispose") {
 56 		if (command == "dispose") {
 57 			var shell = DwtShell.getShell(window);
 58 			shell.dispose(true);
 59 			document.title = "Shell has been disposed";
 60 		}
 61 		var report = [];
 62 		result.success = AjxLeakDetector.report(report);
 63 		if (report.length) {
 64 			DBG.println("Leak detector report.....");
 65 			DBG.printRaw(report.join(""));
 66 		}
 67 		if (result.success) {
 68 			result.message = report.length ? "Problems found. See debug window for details." : "No problems found";
 69 		} else {
 70 			result.message = "Leak detector is not running.";
 71 		}
 72 	} else {
 73 		result.success = false;
 74 		result.message = "Invalid argument, use (begin/end/report)";
 75 	}
 76 	return result;
 77 };
 78 
 79 AjxLeakDetector.begin =
 80 function() {
 81 	if (!AjxLeakDetector._instance) {
 82 		AjxLeakDetector._instance = new AjxLeakDetector();
 83 		return true;
 84 	} else {
 85 		return false;
 86 	}
 87 };
 88 
 89 AjxLeakDetector.end =
 90 function() {
 91 	if (AjxLeakDetector._instance) {
 92 		AjxLeakDetector._instance._removeHooks();
 93 		AjxLeakDetector._instance = null;
 94 		return true;
 95 	} else {
 96 		return false;
 97 	}
 98 };
 99 
100 AjxLeakDetector.report =
101 function(report) {
102 	if (AjxLeakDetector._instance) {
103 		AjxLeakDetector._instance._createReport(report);
104 		return true;
105 	} else {
106 		return false;
107 	}
108 };
109 
110 AjxLeakDetector.prototype._addHooks =
111 function() {
112 	var self = this;
113 
114 	// Hook into __initCtrl
115 	var oldInit = DwtControl.prototype.__initCtrl;
116 	DwtControl.prototype.__initCtrl = function() {
117 		self._controls.push(this);
118 		oldInit.call(this);
119 	};
120 
121 	// Hook into dispose.
122 	var oldDispose = DwtControl.prototype.dispose;
123 	DwtControl.prototype.dispose = function() {
124 		var element = document.getElementById(this.getHTMLElId());
125 		oldDispose.call(this);
126 		self._postDisposeCheck(this, element);
127 	};
128 
129 	// Hook into simple closure
130 	var oldClosure = AjxCallback.simpleClosure;
131 	AjxCallback.simpleClosure = function(func, obj) {
132 		var result = oldClosure.apply(null, arguments);
133 		result.__leakDetectorId = self._nextId++;
134 		var args = [];
135 		for (var i = 0, count = arguments.length; i < count; i++) {
136 			args[i] = arguments[i];
137 		}
138 		self._closures[result.__leakDetectorId] = {
139 			closure: result,
140 			args: args
141 		};
142 		return result;
143 	};
144 
145 	// Create method for undoing this one.
146 	this._removeHooks = function() {
147 		DwtControl.prototype.__initCtrl = oldInit;
148 		DwtControl.prototype.dispose = oldDispose;
149 		AjxCallback.simpleClosure = oldClosure;
150 	};
151 };
152 
153 AjxLeakDetector.prototype._createReport =
154 function(report) {
155 	for (var i = 0, count = this._controls.length; i < count; i++) {
156 		var control = this._controls[i];
157 
158 		// If the control believes it is still in play, make sure the html element is too.
159 		if (!control._disposed) {
160 			var element = document.getElementById(control.getHTMLElId());
161 			if (!element) {
162 				this._log(report, "Detached html element", control);
163 			}
164 		}
165 		// If the control has been disposed, make sure it doesn't directly reference any html elements.
166 		else {
167 			var elementNames = null;
168 			for (var name in control) {
169 				var value = control[name];
170 				if (value && value.tagName) { // I'm using tagName!=null to detect that it's an element.
171 					elementNames = elementNames || [];
172 					elementNames.push(name);
173 				}
174 			}
175 			if (elementNames) {
176 				this._log(report, "Elements referenced by control: " + elementNames.join(", "), control);
177 			}
178 		}
179 	}
180 	for (var i = 0, count = this._closureReport.length; i < count; i++) {
181 		report.push(this._closureReport[i]);
182 	}
183 	if (report.length) {
184 		return report.join("");
185 	} else {
186 		return "Leak detector: no problems detected"; 
187 	}
188 };
189 
190 AjxLeakDetector.prototype._log =
191 function(report, message, control, element) {
192 	report.push(message);
193 	report.push("\n ");
194 	var path = [control];
195 	while (control.parent) {
196 		path.push(control.parent);
197 		control = control.parent;
198 	}
199 	for (var i = path.length -  1; i >= 0; i--) {
200 		report.push(path[i].toString());
201 		if (i > 0) {
202 			report.push("->");
203 		}
204 	}
205 	this._logAttrs(report, path[0]);
206 	report.push("\n------------------------\n");
207 };
208 
209 AjxLeakDetector.prototype._logAttrs =
210 function(report, control) {
211 	var attrMap = {
212 		"DwtLabel" : ["__text", "__imageInfo"]
213 	};
214 
215 	var didIt = false;
216 	for (var className in attrMap) {
217 		if (Dwt.instanceOf(control, className)) {
218 			if (!didIt) {
219 				report.push("{\n");
220 			}
221 			var attrs = attrMap[className];
222 			for (var i = 0, count = attrs.length; i < count; i++) {
223 				report.push(" ");
224 				report.push(attrs[i]);
225 				report.push(": ")
226 				report.push(control[attrs[i]]);
227 				report.push("\n")
228 			}
229 			didIt = true;
230 		}
231 	}
232 	if (didIt) {
233 		report.push("}\n");
234 	}
235 };
236 
237 AjxLeakDetector.prototype._postDisposeCheck =
238 function(control, element) {
239 	var report = [];
240 	if (!element) {
241 		this._log(this._closureReport, "Very bad: control's element not in DOM: " + report.join(""), control);
242 	} else {
243 		this._postDisposeElementCheck(report, element);
244 		if (report.length) {
245 			this._log(this._closureReport, "Suspicioius closure args in control: " + report.join(""), control);
246 		}
247 	}
248 };
249 AjxLeakDetector.prototype._postDisposeElementCheck =
250 function(report, element) {
251 	// Go thru all the element's properties looking for values that are simple closures.
252 	var handlers = null;
253 	for (var name in element) {
254 		var argNames = null;
255 		var value;
256 		try {
257 			value = element[name];
258 		} catch (e) {
259 			// Certain properties aren't readable in ff, probably harmless, but report it...	
260 			DBG.println("AjxLeakDetector: error accessing property: " + name);
261 		}
262 		if (value && value.__leakDetectorId) {
263 			var data = this._closures[value.__leakDetectorId];
264 			if (data) {
265 				// Loop over the args that were passed to the closure...
266 				var args = data.args;
267 				for (var i = 0, count = args.length; i < count; i++) {
268 					var arg = args[i];
269 					if (arg instanceof DwtControl) {
270 						argNames = argNames || [];
271 						argNames.push(arg.toString());
272 					} else if (arg.tagName) { // I'm using tagName!=null to detect that it's an element.
273 						argNames = argNames || [];
274 						argNames.push(arg.tagName);
275 					}
276 				}
277 			}
278 		}
279 		if (argNames) {
280 			handlers = handlers || [];
281 			handlers.push("  ");
282 			handlers.push(name);
283 			handlers.push("(");
284 			handlers.push(argNames.join(","));
285 			handlers.push(")\n");
286 		}
287 	}
288 	if (handlers) {
289 		report.push("The element ");
290 		report.push(element.tagName);
291 		report.push("#");
292 		report.push(element.id || "noId");
293 		report.push("has the following handlers that may cause cirular references: \n");
294 		report.push(handlers.join(""));
295 	}
296 	var children = element.childNodes;
297 	for (var i = 0, count = children.length; i < count; i++) {
298 		 this._postDisposeElementCheck(report, children[i]);
299 	}
300 };
301 
302