1 /* 2 * Copyright (C) 2012 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 /** 32 * @constructor 33 * @extends {WebInspector.Object} 34 * @param {!WebInspector.ResourceTreeModel} resourceTreeModel 35 */ 36 WebInspector.RuntimeModel = function(resourceTreeModel) 37 { 38 resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.FrameAdded, this._frameAdded, this); 39 resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.FrameNavigated, this._frameNavigated, this); 40 resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.FrameDetached, this._frameDetached, this); 41 resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.CachedResourcesLoaded, this._didLoadCachedResources, this); 42 this._frameIdToContextList = {}; 43 } 44 45 WebInspector.RuntimeModel.Events = { 46 FrameExecutionContextListAdded: "FrameExecutionContextListAdded", 47 FrameExecutionContextListRemoved: "FrameExecutionContextListRemoved", 48 } 49 50 WebInspector.RuntimeModel.prototype = { 51 /** 52 * @param {?WebInspector.ExecutionContext} executionContext 53 */ 54 setCurrentExecutionContext: function(executionContext) 55 { 56 this._currentExecutionContext = executionContext; 57 }, 58 59 /** 60 * @return {?WebInspector.ExecutionContext} 61 */ 62 currentExecutionContext: function() 63 { 64 return this._currentExecutionContext; 65 }, 66 67 /** 68 * @return {!Array.<!WebInspector.FrameExecutionContextList>} 69 */ 70 contextLists: function() 71 { 72 return Object.values(this._frameIdToContextList); 73 }, 74 75 /** 76 * @param {!WebInspector.ResourceTreeFrame} frame 77 * @return {!WebInspector.FrameExecutionContextList} 78 */ 79 contextListByFrame: function(frame) 80 { 81 return this._frameIdToContextList[frame.id]; 82 }, 83 84 _frameAdded: function(event) 85 { 86 var frame = event.data; 87 var context = new WebInspector.FrameExecutionContextList(frame); 88 this._frameIdToContextList[frame.id] = context; 89 this.dispatchEventToListeners(WebInspector.RuntimeModel.Events.FrameExecutionContextListAdded, context); 90 }, 91 92 _frameNavigated: function(event) 93 { 94 var frame = event.data; 95 var context = this._frameIdToContextList[frame.id]; 96 if (context) 97 context._frameNavigated(frame); 98 }, 99 100 _frameDetached: function(event) 101 { 102 var frame = event.data; 103 var context = this._frameIdToContextList[frame.id]; 104 if (!context) 105 return; 106 this.dispatchEventToListeners(WebInspector.RuntimeModel.Events.FrameExecutionContextListRemoved, context); 107 delete this._frameIdToContextList[frame.id]; 108 }, 109 110 _didLoadCachedResources: function() 111 { 112 InspectorBackend.registerRuntimeDispatcher(new WebInspector.RuntimeDispatcher(this)); 113 RuntimeAgent.enable(); 114 }, 115 116 _executionContextCreated: function(context) 117 { 118 var contextList = this._frameIdToContextList[context.frameId]; 119 // FIXME(85708): this should never happen 120 if (!contextList) 121 return; 122 contextList._addExecutionContext(new WebInspector.ExecutionContext(context.id, context.name, context.isPageContext)); 123 }, 124 125 /** 126 * @param {string} expression 127 * @param {string} objectGroup 128 * @param {boolean} includeCommandLineAPI 129 * @param {boolean} doNotPauseOnExceptionsAndMuteConsole 130 * @param {boolean} returnByValue 131 * @param {boolean} generatePreview 132 * @param {function(?WebInspector.RemoteObject, boolean, ?RuntimeAgent.RemoteObject=)} callback 133 */ 134 evaluate: function(expression, objectGroup, includeCommandLineAPI, doNotPauseOnExceptionsAndMuteConsole, returnByValue, generatePreview, callback) 135 { 136 if (WebInspector.debuggerModel.selectedCallFrame()) { 137 WebInspector.debuggerModel.evaluateOnSelectedCallFrame(expression, objectGroup, includeCommandLineAPI, doNotPauseOnExceptionsAndMuteConsole, returnByValue, generatePreview, callback); 138 return; 139 } 140 141 if (!expression) { 142 // There is no expression, so the completion should happen against global properties. 143 expression = "this"; 144 } 145 146 /** 147 * @param {?Protocol.Error} error 148 * @param {!RuntimeAgent.RemoteObject} result 149 * @param {boolean=} wasThrown 150 */ 151 function evalCallback(error, result, wasThrown) 152 { 153 if (error) { 154 callback(null, false); 155 return; 156 } 157 158 if (returnByValue) 159 callback(null, !!wasThrown, wasThrown ? null : result); 160 else 161 callback(WebInspector.RemoteObject.fromPayload(result), !!wasThrown); 162 } 163 RuntimeAgent.evaluate(expression, objectGroup, includeCommandLineAPI, doNotPauseOnExceptionsAndMuteConsole, this._currentExecutionContext ? this._currentExecutionContext.id : undefined, returnByValue, generatePreview, evalCallback); 164 }, 165 166 /** 167 * @param {!Element} proxyElement 168 * @param {!Range} wordRange 169 * @param {boolean} force 170 * @param {function(!Array.<string>, number=)} completionsReadyCallback 171 */ 172 completionsForTextPrompt: function(proxyElement, wordRange, force, completionsReadyCallback) 173 { 174 // Pass less stop characters to rangeOfWord so the range will be a more complete expression. 175 var expressionRange = wordRange.startContainer.rangeOfWord(wordRange.startOffset, " =:[({;,!+-*/&|^<>", proxyElement, "backward"); 176 var expressionString = expressionRange.toString(); 177 var prefix = wordRange.toString(); 178 this._completionsForExpression(expressionString, prefix, force, completionsReadyCallback); 179 }, 180 181 /** 182 * @param {string} expressionString 183 * @param {string} prefix 184 * @param {boolean} force 185 * @param {function(!Array.<string>, number=)} completionsReadyCallback 186 */ 187 _completionsForExpression: function(expressionString, prefix, force, completionsReadyCallback) 188 { 189 var lastIndex = expressionString.length - 1; 190 191 var dotNotation = (expressionString[lastIndex] === "."); 192 var bracketNotation = (expressionString[lastIndex] === "["); 193 194 if (dotNotation || bracketNotation) 195 expressionString = expressionString.substr(0, lastIndex); 196 197 if (expressionString && parseInt(expressionString, 10) == expressionString) { 198 // User is entering float value, do not suggest anything. 199 completionsReadyCallback([]); 200 return; 201 } 202 203 if (!prefix && !expressionString && !force) { 204 completionsReadyCallback([]); 205 return; 206 } 207 208 if (!expressionString && WebInspector.debuggerModel.selectedCallFrame()) 209 WebInspector.debuggerModel.getSelectedCallFrameVariables(receivedPropertyNames.bind(this)); 210 else 211 this.evaluate(expressionString, "completion", true, true, false, false, evaluated.bind(this)); 212 213 /** 214 * @this {WebInspector.RuntimeModel} 215 */ 216 function evaluated(result, wasThrown) 217 { 218 if (!result || wasThrown) { 219 completionsReadyCallback([]); 220 return; 221 } 222 223 /** 224 * @param {string} primitiveType 225 * @this {WebInspector.RuntimeModel} 226 */ 227 function getCompletions(primitiveType) 228 { 229 var object; 230 if (primitiveType === "string") 231 object = new String(""); 232 else if (primitiveType === "number") 233 object = new Number(0); 234 else if (primitiveType === "boolean") 235 object = new Boolean(false); 236 else 237 object = this; 238 239 var resultSet = {}; 240 for (var o = object; o; o = o.__proto__) { 241 try { 242 var names = Object.getOwnPropertyNames(o); 243 for (var i = 0; i < names.length; ++i) 244 resultSet[names[i]] = true; 245 } catch (e) { 246 } 247 } 248 return resultSet; 249 } 250 251 if (result.type === "object" || result.type === "function") 252 result.callFunctionJSON(getCompletions, undefined, receivedPropertyNames.bind(this)); 253 else if (result.type === "string" || result.type === "number" || result.type === "boolean") 254 this.evaluate("(" + getCompletions + ")(\"" + result.type + "\")", "completion", false, true, true, false, receivedPropertyNamesFromEval.bind(this)); 255 } 256 257 /** 258 * @param {?WebInspector.RemoteObject} notRelevant 259 * @param {boolean} wasThrown 260 * @param {?RuntimeAgent.RemoteObject=} result 261 * @this {WebInspector.RuntimeModel} 262 */ 263 function receivedPropertyNamesFromEval(notRelevant, wasThrown, result) 264 { 265 if (result && !wasThrown) 266 receivedPropertyNames.call(this, result.value); 267 else 268 completionsReadyCallback([]); 269 } 270 271 /** 272 * @this {WebInspector.RuntimeModel} 273 */ 274 function receivedPropertyNames(propertyNames) 275 { 276 RuntimeAgent.releaseObjectGroup("completion"); 277 if (!propertyNames) { 278 completionsReadyCallback([]); 279 return; 280 } 281 var includeCommandLineAPI = (!dotNotation && !bracketNotation); 282 if (includeCommandLineAPI) { 283 const commandLineAPI = ["dir", "dirxml", "keys", "values", "profile", "profileEnd", "monitorEvents", "unmonitorEvents", "inspect", "copy", "clear", 284 "getEventListeners", "debug", "undebug", "monitor", "unmonitor", "table", "$", "$$", "$x"]; 285 for (var i = 0; i < commandLineAPI.length; ++i) 286 propertyNames[commandLineAPI[i]] = true; 287 } 288 this._reportCompletions(completionsReadyCallback, dotNotation, bracketNotation, expressionString, prefix, Object.keys(propertyNames)); 289 } 290 }, 291 292 /** 293 * @param {function(!Array.<string>, number=)} completionsReadyCallback 294 * @param {boolean} dotNotation 295 * @param {boolean} bracketNotation 296 * @param {string} expressionString 297 * @param {string} prefix 298 * @param {!Array.<string>} properties 299 */ 300 _reportCompletions: function(completionsReadyCallback, dotNotation, bracketNotation, expressionString, prefix, properties) { 301 if (bracketNotation) { 302 if (prefix.length && prefix[0] === "'") 303 var quoteUsed = "'"; 304 else 305 var quoteUsed = "\""; 306 } 307 308 var results = []; 309 310 if (!expressionString) { 311 const keywords = ["break", "case", "catch", "continue", "default", "delete", "do", "else", "finally", "for", "function", "if", "in", 312 "instanceof", "new", "return", "switch", "this", "throw", "try", "typeof", "var", "void", "while", "with"]; 313 properties = properties.concat(keywords); 314 } 315 316 properties.sort(); 317 318 for (var i = 0; i < properties.length; ++i) { 319 var property = properties[i]; 320 321 // Assume that all non-ASCII characters are letters and thus can be used as part of identifier. 322 if (dotNotation && !/^[a-zA-Z_$\u008F-\uFFFF][a-zA-Z0-9_$\u008F-\uFFFF]*$/.test(property)) 323 continue; 324 325 if (bracketNotation) { 326 if (!/^[0-9]+$/.test(property)) 327 property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + quoteUsed; 328 property += "]"; 329 } 330 331 if (property.length < prefix.length) 332 continue; 333 if (prefix.length && !property.startsWith(prefix)) 334 continue; 335 336 results.push(property); 337 } 338 completionsReadyCallback(results); 339 }, 340 341 __proto__: WebInspector.Object.prototype 342 } 343 344 /** 345 * @type {!WebInspector.RuntimeModel} 346 */ 347 WebInspector.runtimeModel; 348 349 /** 350 * @constructor 351 * @implements {RuntimeAgent.Dispatcher} 352 * @param {!WebInspector.RuntimeModel} runtimeModel 353 */ 354 WebInspector.RuntimeDispatcher = function(runtimeModel) 355 { 356 this._runtimeModel = runtimeModel; 357 } 358 359 WebInspector.RuntimeDispatcher.prototype = { 360 executionContextCreated: function(context) 361 { 362 this._runtimeModel._executionContextCreated(context); 363 } 364 } 365 366 /** 367 * @constructor 368 * @extends {WebInspector.Object} 369 */ 370 WebInspector.ExecutionContext = function(id, name, isPageContext) 371 { 372 this.id = id; 373 this.name = (isPageContext && !name) ? "<page context>" : name; 374 this.isMainWorldContext = isPageContext; 375 } 376 377 /** 378 * @param {!WebInspector.ExecutionContext} a 379 * @param {!WebInspector.ExecutionContext} b 380 * @return {number} 381 */ 382 WebInspector.ExecutionContext.comparator = function(a, b) 383 { 384 // Main world context should always go first. 385 if (a.isMainWorldContext) 386 return -1; 387 if (b.isMainWorldContext) 388 return +1; 389 return a.name.localeCompare(b.name); 390 } 391 392 /** 393 * @constructor 394 * @extends {WebInspector.Object} 395 */ 396 WebInspector.FrameExecutionContextList = function(frame) 397 { 398 this._frame = frame; 399 this._executionContexts = []; 400 } 401 402 WebInspector.FrameExecutionContextList.EventTypes = { 403 ContextsUpdated: "ContextsUpdated", 404 ContextAdded: "ContextAdded" 405 } 406 407 WebInspector.FrameExecutionContextList.prototype = 408 { 409 _frameNavigated: function(frame) 410 { 411 this._frame = frame; 412 this._executionContexts = []; 413 this.dispatchEventToListeners(WebInspector.FrameExecutionContextList.EventTypes.ContextsUpdated, this); 414 }, 415 416 /** 417 * @param {!WebInspector.ExecutionContext} context 418 */ 419 _addExecutionContext: function(context) 420 { 421 var insertAt = insertionIndexForObjectInListSortedByFunction(context, this._executionContexts, WebInspector.ExecutionContext.comparator); 422 this._executionContexts.splice(insertAt, 0, context); 423 this.dispatchEventToListeners(WebInspector.FrameExecutionContextList.EventTypes.ContextAdded, this); 424 }, 425 426 executionContexts: function() 427 { 428 return this._executionContexts; 429 }, 430 431 mainWorldContext: function() 432 { 433 return this._executionContexts[0]; 434 }, 435 436 /** 437 * @param {string} securityOrigin 438 */ 439 contextBySecurityOrigin: function(securityOrigin) 440 { 441 for (var i = 0; i < this._executionContexts.length; ++i) { 442 var context = this._executionContexts[i]; 443 if (!context.isMainWorldContext && context.name === securityOrigin) 444 return context; 445 } 446 }, 447 448 get frameId() 449 { 450 return this._frame.id; 451 }, 452 453 get url() 454 { 455 return this._frame.url; 456 }, 457 458 get displayName() 459 { 460 if (!this._frame.parentFrame) 461 return "<top frame>"; 462 var name = this._frame.name || ""; 463 var subtitle = new WebInspector.ParsedURL(this._frame.url).displayName; 464 if (subtitle) { 465 if (!name) 466 return subtitle; 467 return name + "( " + subtitle + " )"; 468 } 469 return "<iframe>"; 470 }, 471 472 __proto__: WebInspector.Object.prototype 473 } 474