Ticket #5317: 5317.patch
File 5317.patch, 21.7 KB (added by , 14 years ago) |
---|
-
_dev/leakDetector/detector.js
Property changes on: _dev\leakDetector ___________________________________________________________________ Added: bugtraq:label + Ticket Added: bugtraq:url + http://dev.fckeditor.net/ticket/%BUGID% Added: webviewer:pathrevision + http://dev.fckeditor.net/browser/%PATH%?rev=%REVISION% Added: webviewer:revision + http://dev.fckeditor.net/changeset/%REVISION% Added: bugtraq:logregex + (?:ticket: *|#)(\d+) *(?:, *(\d+))*
1 2 var garbageDetector = { 3 4 getStackTrace: function() 5 { 6 if ( !garbageDetector.getStack ) 7 return ['']; 8 9 // Skip the first two calls (instrumentation) and get only 5 levels deep 10 return printStackTrace().slice(2, 7); 11 }, 12 13 // Hook on special CKEDITOR methods 14 init: function() 15 { 16 // 17 this.getStack = false; 18 19 // tools.addFunction et.al 20 // We store in this object every function that it's passed to tools.addFunction 21 // And then check if it's properly removed. 22 this.functionsStackTraces = {}; 23 this.DOMObjects = {}; 24 CKEDITOR.tools.addFunction = CKEDITOR.tools.override( CKEDITOR.tools.addFunction, function( addOriginal ) 25 { 26 return function( fn, scope ) 27 { 28 var index = addOriginal.apply( this, arguments ); 29 garbageDetector.functionsStackTraces[ index ] = garbageDetector.getStackTrace(); 30 return index; 31 }; 32 }); 33 34 CKEDITOR.tools.removeFunction = CKEDITOR.tools.override( CKEDITOR.tools.removeFunction, function( removeOriginal ) 35 { 36 return function( ref ) 37 { 38 removeOriginal.apply( this, arguments ); 39 delete garbageDetector.functionsStackTraces[ ref ]; 40 }; 41 }); 42 43 // CKEDITOR.dom.domObject listeners 44 CKEDITOR.dom.domObject.prototype.on = CKEDITOR.tools.override( CKEDITOR.dom.domObject.prototype.on, function( onOriginal ) 45 { 46 return function() 47 { 48 var value = onOriginal.apply( this, arguments ); 49 garbageDetector.storeUsedObject( this, garbageDetector.getStackTrace() ); 50 return value; 51 }; 52 }); 53 54 CKEDITOR.dom.domObject.prototype.removeAllListeners = CKEDITOR.tools.override( CKEDITOR.dom.domObject.prototype.removeAllListeners, function( removeOriginal ) 55 { 56 return function() 57 { 58 garbageDetector.clearUsedObject( this ); 59 removeOriginal.apply( this ); 60 }; 61 }); 62 63 }, 64 65 // Track usage and clean up of domObject elements 66 storeUsedObject : function( obj, stack ) 67 { 68 // Warning: any object might get multiple uses, but we only store one callstack 69 garbageDetector.DOMObjects[ obj.$._cke_expando ] = { obj : obj, stack : stack }; 70 }, 71 clearUsedObject : function( obj ) 72 { 73 delete garbageDetector.DOMObjects[ obj.$._cke_expando ]; 74 }, 75 76 // Take a snapshot of the initial object 77 // We are interested in what has been modified during the test, not after it. 78 startLeaksCheck : function() 79 { 80 this.getStack = true; 81 82 // Reset previous data 83 garbageDetector.DOMObjects = {}; 84 85 var reference = {}; 86 reference.functions = CKEDITOR.tools.clone( this.functionsStackTraces ); 87 reference.events = CKEDITOR.tools.clone( CKEDITOR._.events ); 88 reference.CKEDITOR = this.safeDeepClone( CKEDITOR, 'CKEDITOR' ); 89 this.reference = reference; 90 }, 91 92 93 // Copied from CKEDITOR.tools.clone, but will skip any $ as they can't be cloned 94 safeDeepClone : function( obj, name ) 95 { 96 var clone; 97 98 // Array. 99 if ( obj && ( obj instanceof Array ) ) 100 { 101 clone = []; 102 103 for ( var i = 0 ; i < obj.length ; i++ ) 104 clone[ i ] = this.safeDeepClone( obj[ i ], name + '[' + i + ']' ); 105 106 return clone; 107 } 108 109 // "Static" types. 110 if ( obj === null 111 || ( typeof obj == 'string' ) 112 || ( obj instanceof String ) 113 || ( obj instanceof Number ) 114 || ( obj instanceof Boolean ) 115 || ( obj instanceof Date ) 116 || ( obj instanceof RegExp) ) 117 { 118 return obj; 119 } 120 121 // Objects. 122 clone = {}; 123 124 for ( var propertyName in obj ) 125 { 126 var property = obj[ propertyName ]; 127 // avoid infinite loops 128 if ( propertyName == 'next' || propertyName == 'previous' ) 129 { 130 continue; 131 } 132 // Special protection for the detector. 133 if ( propertyName == '$' || propertyName == 'prototype' ) 134 { 135 clone[ propertyName ] = {};// something to note that it existed 136 continue; 137 } 138 if ( propertyName == 'editor' && name != 'CKEDITOR' ) 139 { 140 clone[ propertyName ] = {};// something to note that it existed 141 continue; 142 } 143 if ( propertyName == 'toolbar' && name != 'CKEDITOR.plugins.registered' ) 144 { 145 clone[ propertyName ] = {};// something to note that it existed 146 continue; 147 } 148 149 clone[ propertyName ] = this.safeDeepClone( property , name + '.' + propertyName ); 150 } 151 152 return clone; 153 }, 154 155 156 endLeaksCheck : function() 157 { 158 this.getStack = false; 159 var messages = []; 160 161 // Check for created functions: 162 var oldFunctions = this.reference.functions; 163 var newFunctionsData = this.functionsStackTraces ; 164 for ( var i in newFunctionsData ) 165 { 166 if ( !oldFunctions[ i ] ) 167 { 168 messages.push( '<h2>Leaked function ' + i + '</h2>' ); 169 messages.push( this.printStack( newFunctionsData[ i ] ) ); 170 } 171 } 172 173 // Check for global events 174 var oldEvents = this.reference.events; 175 var newEvents = CKEDITOR._.events ; 176 177 for ( var e in newEvents ) 178 { 179 var data = newEvents[ e ]; 180 var oldData = oldEvents[ e ]; 181 if (!oldData) 182 { 183 messages.push( '<h2>New event handler in CKEDITOR for ' + e + ' event</h2>' ); 184 } 185 else 186 { 187 if (data.listeners.length > oldData.listeners.length) 188 { 189 messages.push( '<h2>New listener in CKEDITOR for ' + e + ' event</h2>'); 190 } 191 } 192 } 193 194 for ( o in this.DOMObjects ) 195 { 196 var names = this.getListenerNames( this.DOMObjects[ o ].obj ); 197 if ( names.length > 0 ) 198 { 199 // The object might have been added, but the listeners cleaned up. 200 messages.push( '<h2>Listeners not cleaned up on ' + this.outputObject( this.DOMObjects[ o ].obj.$ ) + '</h2>' ); 201 messages.push( '<p>Events hooked:<br>' + names.join( '<br>' ) ); 202 messages.push( this.printStack( this.DOMObjects[ o ].stack ) + '</p>' ); 203 if (window.console) console.log( this.DOMObjects[ o ].obj ); 204 } 205 } 206 207 // Any other property set on the global CKEDITOR 208 messages.push( this.compareObject( this.reference.CKEDITOR, CKEDITOR, 'CKEDITOR' ) ); 209 // messages.push( this.compareObject( this.reference.CKEDITOR.ui.button._ , CKEDITOR.ui.button._ , 'CKEDITOR.ui.button._' ) ); 210 211 messages.push( '<h2>End of report</h2>' ); 212 return messages.join( '\r\n' ); 213 }, 214 215 compareObject : function( reference, current, title) 216 { 217 /* 218 console.log('checking ' + title); 219 console.log(typeof current); 220 console.log( current ); 221 */ 222 var currentType = (typeof current); 223 if ( currentType == 'string' ) return ''; 224 225 226 var messages = []; 227 for( var o in current ) 228 { 229 // Don't check for native objects 230 if ( o == '$' ) 231 continue; 232 233 // Causes infinite recursion 234 if ( o == 'prototype' ) 235 continue; 236 237 if ( !(o in reference) ) 238 messages.push( '<h2>New element "' + title + '.' + o + '"</h2>' ); 239 else 240 { 241 var obj = current[ o ], 242 old = reference[ o ]; 243 244 if ( !obj ) 245 continue; 246 247 if ( obj instanceof Array ) 248 { 249 for ( var i = old.length ; i < obj.length ; i++ ) 250 { 251 if ( obj[ i ] ) 252 messages.push( '<h2>New contents in array ' + title + '.' + o + '[' + i + ']</h2>' ); 253 } 254 continue; 255 } 256 257 // "Static" types. 258 if ( obj === null 259 || ( obj instanceof String ) 260 || ( obj instanceof Number ) 261 || ( obj instanceof Boolean ) 262 || ( obj instanceof Date ) 263 || ( obj instanceof RegExp) ) 264 { 265 continue; 266 } 267 268 messages.push( this.compareObject( old, obj, title + '.' + o) ); 269 } 270 } 271 return messages.join( '\r\n' ); 272 }, 273 274 getListenerNames : function( obj ) 275 { 276 var names = []; 277 var nativeListeners = obj.getCustomData( '_cke_nativeListeners' ); 278 for ( eventName in nativeListeners) 279 { 280 names.push( eventName ); 281 } 282 return names; 283 }, 284 285 outputObject : function( obj ) 286 { 287 var text = '<' + obj.nodeName; 288 text += ' _cke_expando="' + obj. _cke_expando + '"'; 289 if (obj.id) text += ' id="' + obj.id + '"'; 290 if (obj.className) text += ' class="' + obj.className + '"'; 291 text += '>'; 292 return '<span class="object">' + text + '</span>'; 293 }, 294 295 printStack : function( data ) 296 { 297 var text = data.join( '\r\n' ); 298 text = text.replace( /</g , '<' ).replace( />/g , '>' ); 299 return '<p class="stack">' + text + '</p>'; 300 } 301 302 }; 303 No newline at end of file -
_dev/leakDetector/leakDetector.html
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 2 <!-- 3 Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved. 4 For licensing, see LICENSE.html or http://ckeditor.com/license 5 --> 6 <html xmlns="http://www.w3.org/1999/xhtml"> 7 <head> 8 <title>Leak Detector - CKEditor Sample</title> 9 <meta content="text/html; charset=utf-8" http-equiv="content-type" /> 10 <script type="text/javascript" src="../../ckeditor_source.js"></script> 11 <script type="text/javascript" src="stacktrace.js"></script> 12 <script type="text/javascript" src="detector.js"></script> 13 <script type="text/javascript"> 14 //<![CDATA[ 15 16 garbageDetector.init() 17 18 var editor; 19 var allPlugins = 'about,a11yhelp,basicstyles,blockquote,button,clipboard,colorbutton,colordialog,contextmenu,div,elementspath,enterkey,entities,filebrowser,find,flash,font,format,forms,horizontalrule,htmldataprocessor,image,indent,justify,keystrokes,link,list,maximize,newpage,pagebreak,pastefromword,pastetext,popup,preview,print,removeformat,resize,save,scayt,smiley,showblocks,showborders,sourcearea,stylescombo,table,tabletools,specialchar,tab,templates,toolbar,undo,wysiwygarea,wsc'; 20 21 var removePlugins = ''; 22 23 // toolbarCanCollapse : false, 24 // startupMode : 'source', 25 // extraPlugins: 'selection', 26 27 var config = { 28 removePlugins : removePlugins, 29 customConfig : '' 30 } 31 32 function createEditor() 33 { 34 if ( editor ) 35 return; 36 37 var html = document.getElementById( 'editorcontents' ).innerHTML; 38 39 editor = CKEDITOR.appendTo( 'editor', config, html ); 40 } 41 42 function removeEditor() 43 { 44 if ( !editor ) 45 return; 46 47 // Retrieve the editor contents. In an Ajax application, this data would be 48 // sent to the server or used in any other way. 49 document.getElementById( 'editorcontents' ).innerHTML = editor.getData(); 50 // document.getElementById( 'contents' ).style.display = ''; 51 52 // Destroy the editor. 53 editor.destroy(); 54 editor = null; 55 } 56 57 58 function initTestLeak() 59 { 60 document.getElementById( 'output' ).innerHTML = ''; 61 document.getElementById( 'init' ).disabled = true; 62 63 // First create an instance, destroy it and then create the instance that will be tested 64 CKEDITOR.on( 'instanceReady' , DestroyFirstEditor ); 65 createEditor(); 66 } 67 68 69 // If the loop with plugins is used, it will automatically destroy the editor 70 function DestroyFirstEditor() { 71 // Set up listener to create the real instance and start tests 72 editor.on( 'destroy' , function () { 73 window.setTimeout( startSecondEditor, 200 ); 74 } ) 75 window.setTimeout( removeEditor, 200 ); 76 } 77 78 function startSecondEditor() 79 { 80 CKEDITOR.removeListener( 'instanceReady' , DestroyFirstEditor ); 81 garbageDetector.startLeaksCheck(); 82 // Create the instance to test 83 createEditor(); 84 document.getElementById( 'end' ).disabled = false; 85 } 86 87 function endTestLeak() 88 { 89 if (!editor) 90 { 91 document.getElementById( 'init' ).disabled = false; 92 document.getElementById( 'end' ).disabled = true; 93 return; 94 } 95 96 editor.on( 'destroy' , function () { 97 window.setTimeout( checkLeaks, 50 ); 98 } ); 99 100 removeEditor(); 101 } 102 103 function checkLeaks() 104 { 105 var text = garbageDetector.endLeaksCheck(); 106 document.getElementById( 'output' ).innerHTML = text; 107 108 document.getElementById( 'init' ).disabled = false; 109 document.getElementById( 'end' ).disabled = true; 110 } 111 112 //]]> 113 </script> 114 <style type="text/css"> 115 h2 { 116 margin-bottom:0.4em; 117 } 118 .stack { 119 white-space:pre; 120 font-size:80%; 121 } 122 </style> 123 </head> 124 <body> 125 <h1> 126 CKEditor Sample 127 </h1> 128 129 <p> 130 <input onclick="initTestLeak();" id="init" type="button" value="Start leak check" /> 131 <input onclick="endTestLeak();" id="end" type="button" value="End leak check" disabled="disabled"/> 132 </p> 133 <!-- This div will hold the editor. --> 134 <div id="editor"> 135 </div> 136 <div id="contents" style="display: none"> 137 <p> 138 Edited Contents:</p> 139 <!-- This div will be used to display the editor contents. --> 140 <div id="editorcontents"> 141 </div> 142 </div> 143 <div id="output"></div> 144 <div id="footer"> 145 <hr /> 146 <p> 147 CKEditor - The text editor for Internet - <a href="http://ckeditor.com/">http://ckeditor.com</a> 148 </p> 149 <p id="copy"> 150 Copyright © 2003-2010, <a href="http://cksource.com/">CKSource</a> - Frederico 151 Knabben. All rights reserved. 152 </p> 153 </div> 154 </body> 155 </html> -
_dev/leakDetector/stacktrace.js
1 // Domain Public by Eric Wendelin http://eriwen.com/ (2008) 2 // Luke Smith http://lucassmith.name/ (2008) 3 // Loic Dachary <loic@dachary.org> (2008) 4 // Johan Euphrosine <proppy@aminche.com> (2008) 5 // 6 // Information and discussions 7 // http://jspoker.pokersource.info/skin/test-printstacktrace.html 8 // http://eriwen.com/javascript/js-stack-trace/ 9 // http://pastie.org/253058 10 // http://browsershots.org/http://jspoker.pokersource.info/skin/test-printstacktrace.html 11 // 12 13 // 14 // guessFunctionNameFromLines comes from firebug 15 // 16 // Software License Agreement (BSD License) 17 // 18 // Copyright (c) 2007, Parakey Inc. 19 // All rights reserved. 20 // 21 // Redistribution and use of this software in source and binary forms, with or without modification, 22 // are permitted provided that the following conditions are met: 23 // 24 // * Redistributions of source code must retain the above 25 // copyright notice, this list of conditions and the 26 // following disclaimer. 27 // 28 // * Redistributions in binary form must reproduce the above 29 // copyright notice, this list of conditions and the 30 // following disclaimer in the documentation and/or other 31 // materials provided with the distribution. 32 // 33 // * Neither the name of Parakey Inc. nor the names of its 34 // contributors may be used to endorse or promote products 35 // derived from this software without specific prior 36 // written permission of Parakey Inc. 37 // 38 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 39 // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 40 // FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 41 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 42 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 43 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 44 // IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 45 // OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 46 47 function printStackTrace(options) { 48 if (options && options.guess) { 49 var p = new printStackTrace.implementation(); 50 var result = p.run(); 51 return p.guessFunctions(result); 52 } 53 return (new printStackTrace.implementation()).run(); 54 } 55 56 printStackTrace.implementation = function() {}; 57 58 printStackTrace.implementation.prototype = { 59 run: function() { 60 mode = this.mode(); 61 if(mode != 'other') { 62 try {(0)();} catch (e) { 63 return this[mode](e); 64 } 65 } else { 66 return this.other(arguments.callee); 67 } 68 }, 69 70 mode: function() { 71 var mode; 72 try {(0)();} catch (e) { 73 if (e.arguments) { 74 mode = 'chrome'; 75 } else if (e.stack) { 76 mode = 'firefox'; 77 } else { 78 mode = 'other'; 79 } 80 } 81 return mode; 82 }, 83 84 chrome: function(e) { 85 return e.stack.replace(/^.*?\n/,''). 86 replace(/^.*?\n/,''). 87 replace(/^.*?\n/,''). 88 replace(/^[^\(]+?[\n$]/gm,''). 89 replace(/^\s+at\s+/gm,''). 90 replace(/^Object.<anonymous>/gm,'{anonymous}()'). 91 split("\n"); 92 }, 93 94 firefox: function(e) { 95 return e.stack.replace(/^.*?\n/,''). 96 replace(/(?:\n@:0)?\s+$/m,''). 97 replace(/^\(/gm,'{anonymous}('). 98 split("\n"); 99 }, 100 101 other: function(curr) { 102 var ANON = "{anonymous}", 103 fnRE = /function\s*([\w\-$]+)?\s*\(/i, 104 stack = [],j=0,fn,args; 105 106 var maxStackSize = 7; 107 while (curr && stack.length < maxStackSize) { 108 fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON; 109 args = Array.prototype.slice.call(curr['arguments']); 110 stack[j++] = fn + '(' + printStackTrace.implementation.prototype.stringifyArguments(args) + ')'; 111 112 //Opera bug: if curr.caller does not exist, Opera returns curr (WTF) 113 if (curr === curr.caller && window.opera) { 114 break; 115 } 116 curr = curr.caller; 117 } 118 return stack; 119 }, 120 121 stringifyArguments: function(args) { 122 for (var i = 0; i < args.length; ++i) { 123 var argument = args[i]; 124 if (typeof argument == 'object') { 125 args[i] = '#object'; 126 } else if (typeof argument == 'function') { 127 args[i] = '#function'; 128 } else if (typeof argument == 'string') { 129 args[i] = '"'+argument+'"'; 130 } 131 } 132 return args.join(','); 133 }, 134 135 sourceCache: {}, 136 137 ajax: function(url) { 138 var req = this.createXMLHTTPObject(); 139 if (!req) { 140 return; 141 } 142 req.open('GET', url, false); 143 req.setRequestHeader("User-Agent", "XMLHTTP/1.0"); 144 req.send(''); 145 return req.responseText; 146 }, 147 148 createXMLHTTPObject: function() { 149 var XMLHttpFactories = [ 150 function () { 151 return new XMLHttpRequest(); 152 }, 153 function () { 154 return new ActiveXObject("Msxml2.XMLHTTP"); 155 }, 156 function () { 157 return new ActiveXObject("Msxml3.XMLHTTP"); 158 }, 159 function () { 160 return new ActiveXObject("Microsoft.XMLHTTP"); 161 } 162 ]; 163 var xmlhttp = false; 164 for (var i = 0; i < XMLHttpFactories.length; i++) { 165 try { 166 xmlhttp = XMLHttpFactories[i](); 167 } 168 catch (e) { 169 e = null; 170 continue; 171 } 172 break; 173 } 174 return xmlhttp; 175 }, 176 177 getSource: function(url) { 178 var self = this; 179 if (!(url in self.sourceCache)) { 180 self.sourceCache[url] = self.ajax(url).split("\n"); 181 } 182 return self.sourceCache[url]; 183 }, 184 185 guessFunctions: function(stack) { 186 for (var i = 0; i < stack.length; ++i) { 187 var reStack = /{anonymous}\(.*\)@(.*):(\d+)/; 188 var frame = stack[i]; 189 var m = reStack.exec(frame); 190 if (m) { 191 var file = m[1]; 192 var lineno = m[2]; 193 if (file && lineno) { 194 var functionName = this.guessFunctionName(file, lineno); 195 stack[i] = frame.replace('{anonymous}', functionName); 196 } 197 } 198 } 199 return stack; 200 }, 201 202 guessFunctionName: function(url, lineNo) { 203 var source; 204 try { 205 source = this.getSource(url); 206 } catch (e) { 207 return 'getSource failed with url: ' + url + ', exception: ' + e.toString(); 208 } 209 return this.guessFunctionNameFromLines(lineNo, source); 210 }, 211 212 guessFunctionNameFromLines: function(lineNo, source) { 213 var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/; 214 var reGuessFunction = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(function|eval|new Function)/; 215 // Walk backwards from the first line in the function until we find the line which 216 // matches the pattern above, which is the function definition 217 var line = ""; 218 var maxLines = 10; 219 for (var i = 0; i < maxLines; ++i) { 220 line = source[lineNo-i] + line; 221 if (line !== undefined) { 222 var m = reGuessFunction.exec(line); 223 if (m) { 224 return m[1]; 225 } else { 226 m = reFunctionArgNames.exec(line); 227 } 228 if (m && m[1]) { 229 return m[1]; 230 } 231 } 232 } 233 return "(?)"; 234 } 235 };