1 /* 2 * Copyright (C) 2007, 2008 Apple 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 6 * are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright 11 * notice, this list of conditions and the following disclaimer in the 12 * documentation and/or other materials provided with the distribution. 13 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 14 * its contributors may be used to endorse or promote products derived 15 * from this software without specific prior written permission. 16 * 17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 */ 28 WebInspector.Resource = function(identifier, url) 29 { 30 this.identifier = identifier; 31 this.url = url; 32 this._startTime = -1; 33 this._endTime = -1; 34 this._category = WebInspector.resourceCategories.other; 35 this._pendingContentCallbacks = []; 36 this.history = []; 37 } 38 39 // Keep these in sync with WebCore::InspectorResource::Type 40 WebInspector.Resource.Type = { 41 Document: 0, 42 Stylesheet: 1, 43 Image: 2, 44 Font: 3, 45 Script: 4, 46 XHR: 5, 47 WebSocket: 7, 48 Other: 8, 49 50 isTextType: function(type) 51 { 52 return (type === this.Document) || (type === this.Stylesheet) || (type === this.Script) || (type === this.XHR); 53 }, 54 55 toUIString: function(type) 56 { 57 switch (type) { 58 case this.Document: 59 return WebInspector.UIString("Document"); 60 case this.Stylesheet: 61 return WebInspector.UIString("Stylesheet"); 62 case this.Image: 63 return WebInspector.UIString("Image"); 64 case this.Font: 65 return WebInspector.UIString("Font"); 66 case this.Script: 67 return WebInspector.UIString("Script"); 68 case this.XHR: 69 return WebInspector.UIString("XHR"); 70 case this.WebSocket: 71 return WebInspector.UIString("WebSocket"); 72 case this.Other: 73 default: 74 return WebInspector.UIString("Other"); 75 } 76 }, 77 78 // Returns locale-independent string identifier of resource type (primarily for use in extension API). 79 // The IDs need to be kept in sync with webInspector.resoureces.Types object in ExtensionAPI.js. 80 toString: function(type) 81 { 82 switch (type) { 83 case this.Document: 84 return "document"; 85 case this.Stylesheet: 86 return "stylesheet"; 87 case this.Image: 88 return "image"; 89 case this.Font: 90 return "font"; 91 case this.Script: 92 return "script"; 93 case this.XHR: 94 return "xhr"; 95 case this.WebSocket: 96 return "websocket"; 97 case this.Other: 98 default: 99 return "other"; 100 } 101 } 102 } 103 104 WebInspector.Resource._domainModelBindings = []; 105 106 WebInspector.Resource.registerDomainModelBinding = function(type, binding) 107 { 108 WebInspector.Resource._domainModelBindings[type] = binding; 109 } 110 111 WebInspector.Resource.Events = { 112 RevisionAdded: 0 113 } 114 115 WebInspector.Resource.prototype = { 116 get url() 117 { 118 return this._url; 119 }, 120 121 set url(x) 122 { 123 if (this._url === x) 124 return; 125 126 this._url = x; 127 delete this._parsedQueryParameters; 128 129 var parsedURL = x.asParsedURL(); 130 this.domain = parsedURL ? parsedURL.host : ""; 131 this.path = parsedURL ? parsedURL.path : ""; 132 this.lastPathComponent = ""; 133 if (parsedURL && parsedURL.path) { 134 // First cut the query params. 135 var path = parsedURL.path; 136 var indexOfQuery = path.indexOf("?"); 137 if (indexOfQuery !== -1) 138 path = path.substring(0, indexOfQuery); 139 140 // Then take last path component. 141 var lastSlashIndex = path.lastIndexOf("/"); 142 if (lastSlashIndex !== -1) 143 this.lastPathComponent = path.substring(lastSlashIndex + 1); 144 } 145 this.lastPathComponentLowerCase = this.lastPathComponent.toLowerCase(); 146 }, 147 148 get documentURL() 149 { 150 return this._documentURL; 151 }, 152 153 set documentURL(x) 154 { 155 this._documentURL = x; 156 }, 157 158 get displayName() 159 { 160 if (this._displayName) 161 return this._displayName; 162 this._displayName = this.lastPathComponent; 163 if (!this._displayName) 164 this._displayName = this.displayDomain; 165 if (!this._displayName && this.url) 166 this._displayName = this.url.trimURL(WebInspector.mainResource ? WebInspector.mainResource.domain : ""); 167 if (this._displayName === "/") 168 this._displayName = this.url; 169 return this._displayName; 170 }, 171 172 get displayDomain() 173 { 174 // WebInspector.Database calls this, so don't access more than this.domain. 175 if (this.domain && (!WebInspector.mainResource || (WebInspector.mainResource && this.domain !== WebInspector.mainResource.domain))) 176 return this.domain; 177 return ""; 178 }, 179 180 get startTime() 181 { 182 return this._startTime || -1; 183 }, 184 185 set startTime(x) 186 { 187 this._startTime = x; 188 }, 189 190 get responseReceivedTime() 191 { 192 return this._responseReceivedTime || -1; 193 }, 194 195 set responseReceivedTime(x) 196 { 197 this._responseReceivedTime = x; 198 }, 199 200 get endTime() 201 { 202 return this._endTime || -1; 203 }, 204 205 set endTime(x) 206 { 207 if (this.timing && this.timing.requestTime) { 208 // Check against accurate responseReceivedTime. 209 this._endTime = Math.max(x, this.responseReceivedTime); 210 } else { 211 // Prefer endTime since it might be from the network stack. 212 this._endTime = x; 213 if (this._responseReceivedTime > x) 214 this._responseReceivedTime = x; 215 } 216 }, 217 218 get duration() 219 { 220 if (this._endTime === -1 || this._startTime === -1) 221 return -1; 222 return this._endTime - this._startTime; 223 }, 224 225 get latency() 226 { 227 if (this._responseReceivedTime === -1 || this._startTime === -1) 228 return -1; 229 return this._responseReceivedTime - this._startTime; 230 }, 231 232 get receiveDuration() 233 { 234 if (this._endTime === -1 || this._responseReceivedTime === -1) 235 return -1; 236 return this._endTime - this._responseReceivedTime; 237 }, 238 239 get resourceSize() 240 { 241 return this._resourceSize || 0; 242 }, 243 244 set resourceSize(x) 245 { 246 this._resourceSize = x; 247 }, 248 249 get transferSize() 250 { 251 if (this.cached) 252 return 0; 253 if (this.statusCode === 304) // Not modified 254 return this.responseHeadersSize; 255 if (this._transferSize !== undefined) 256 return this._transferSize; 257 // If we did not receive actual transfer size from network 258 // stack, we prefer using Content-Length over resourceSize as 259 // resourceSize may differ from actual transfer size if platform's 260 // network stack performed decoding (e.g. gzip decompression). 261 // The Content-Length, though, is expected to come from raw 262 // response headers and will reflect actual transfer length. 263 // This won't work for chunked content encoding, so fall back to 264 // resourceSize when we don't have Content-Length. This still won't 265 // work for chunks with non-trivial encodings. We need a way to 266 // get actual transfer size from the network stack. 267 var bodySize = Number(this.responseHeaders["Content-Length"] || this.resourceSize); 268 return this.responseHeadersSize + bodySize; 269 }, 270 271 increaseTransferSize: function(x) 272 { 273 this._transferSize = (this._transferSize || 0) + x; 274 }, 275 276 get finished() 277 { 278 return this._finished; 279 }, 280 281 set finished(x) 282 { 283 if (this._finished === x) 284 return; 285 286 this._finished = x; 287 288 if (x) { 289 this._checkWarnings(); 290 this.dispatchEventToListeners("finished"); 291 if (this._pendingContentCallbacks.length) 292 this._innerRequestContent(); 293 } 294 }, 295 296 get failed() 297 { 298 return this._failed; 299 }, 300 301 set failed(x) 302 { 303 this._failed = x; 304 }, 305 306 get canceled() 307 { 308 return this._canceled; 309 }, 310 311 set canceled(x) 312 { 313 this._canceled = x; 314 }, 315 316 get category() 317 { 318 return this._category; 319 }, 320 321 set category(x) 322 { 323 this._category = x; 324 }, 325 326 get cached() 327 { 328 return this._cached; 329 }, 330 331 set cached(x) 332 { 333 this._cached = x; 334 if (x) 335 delete this._timing; 336 }, 337 338 get timing() 339 { 340 return this._timing; 341 }, 342 343 set timing(x) 344 { 345 if (x && !this._cached) { 346 // Take startTime and responseReceivedTime from timing data for better accuracy. 347 // Timing's requestTime is a baseline in seconds, rest of the numbers there are ticks in millis. 348 this._startTime = x.requestTime; 349 this._responseReceivedTime = x.requestTime + x.receiveHeadersEnd / 1000.0; 350 351 this._timing = x; 352 this.dispatchEventToListeners("timing changed"); 353 } 354 }, 355 356 get mimeType() 357 { 358 return this._mimeType; 359 }, 360 361 set mimeType(x) 362 { 363 this._mimeType = x; 364 }, 365 366 get type() 367 { 368 return this._type; 369 }, 370 371 set type(x) 372 { 373 if (this._type === x) 374 return; 375 376 this._type = x; 377 378 switch (x) { 379 case WebInspector.Resource.Type.Document: 380 this.category = WebInspector.resourceCategories.documents; 381 break; 382 case WebInspector.Resource.Type.Stylesheet: 383 this.category = WebInspector.resourceCategories.stylesheets; 384 break; 385 case WebInspector.Resource.Type.Script: 386 this.category = WebInspector.resourceCategories.scripts; 387 break; 388 case WebInspector.Resource.Type.Image: 389 this.category = WebInspector.resourceCategories.images; 390 break; 391 case WebInspector.Resource.Type.Font: 392 this.category = WebInspector.resourceCategories.fonts; 393 break; 394 case WebInspector.Resource.Type.XHR: 395 this.category = WebInspector.resourceCategories.xhr; 396 break; 397 case WebInspector.Resource.Type.WebSocket: 398 this.category = WebInspector.resourceCategories.websockets; 399 break; 400 case WebInspector.Resource.Type.Other: 401 default: 402 this.category = WebInspector.resourceCategories.other; 403 break; 404 } 405 }, 406 407 get requestHeaders() 408 { 409 return this._requestHeaders || {}; 410 }, 411 412 set requestHeaders(x) 413 { 414 this._requestHeaders = x; 415 delete this._sortedRequestHeaders; 416 delete this._requestCookies; 417 delete this._responseHeadersSize; 418 419 this.dispatchEventToListeners("requestHeaders changed"); 420 }, 421 422 get requestHeadersText() 423 { 424 return this._requestHeadersText; 425 }, 426 427 set requestHeadersText(x) 428 { 429 this._requestHeadersText = x; 430 delete this._responseHeadersSize; 431 432 this.dispatchEventToListeners("requestHeaders changed"); 433 }, 434 435 get requestHeadersSize() 436 { 437 if (typeof(this._requestHeadersSize) === "undefined") { 438 if (this._requestHeadersText) 439 this._requestHeadersSize = this._requestHeadersText.length; 440 else 441 this._requestHeadersSize = this._headersSize(this._requestHeaders) 442 } 443 return this._requestHeadersSize; 444 }, 445 446 get sortedRequestHeaders() 447 { 448 if (this._sortedRequestHeaders !== undefined) 449 return this._sortedRequestHeaders; 450 451 this._sortedRequestHeaders = []; 452 for (var key in this.requestHeaders) 453 this._sortedRequestHeaders.push({header: key, value: this.requestHeaders[key]}); 454 this._sortedRequestHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) }); 455 456 return this._sortedRequestHeaders; 457 }, 458 459 requestHeaderValue: function(headerName) 460 { 461 return this._headerValue(this.requestHeaders, headerName); 462 }, 463 464 get requestCookies() 465 { 466 if (!this._requestCookies) 467 this._requestCookies = WebInspector.CookieParser.parseCookie(this.requestHeaderValue("Cookie")); 468 return this._requestCookies; 469 }, 470 471 get requestFormData() 472 { 473 return this._requestFormData; 474 }, 475 476 set requestFormData(x) 477 { 478 this._requestFormData = x; 479 delete this._parsedFormParameters; 480 }, 481 482 get responseHeaders() 483 { 484 return this._responseHeaders || {}; 485 }, 486 487 set responseHeaders(x) 488 { 489 this._responseHeaders = x; 490 delete this._responseHeadersSize; 491 delete this._sortedResponseHeaders; 492 delete this._responseCookies; 493 494 this.dispatchEventToListeners("responseHeaders changed"); 495 }, 496 497 get responseHeadersText() 498 { 499 return this._responseHeadersText; 500 }, 501 502 set responseHeadersText(x) 503 { 504 this._responseHeadersText = x; 505 delete this._responseHeadersSize; 506 507 this.dispatchEventToListeners("responseHeaders changed"); 508 }, 509 510 get responseHeadersSize() 511 { 512 if (typeof(this._responseHeadersSize) === "undefined") { 513 if (this._responseHeadersText) 514 this._responseHeadersSize = this._responseHeadersText.length; 515 else 516 this._responseHeadersSize = this._headersSize(this._responseHeaders) 517 } 518 return this._responseHeadersSize; 519 }, 520 521 522 get sortedResponseHeaders() 523 { 524 if (this._sortedResponseHeaders !== undefined) 525 return this._sortedResponseHeaders; 526 527 this._sortedResponseHeaders = []; 528 for (var key in this.responseHeaders) 529 this._sortedResponseHeaders.push({header: key, value: this.responseHeaders[key]}); 530 this._sortedResponseHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) }); 531 532 return this._sortedResponseHeaders; 533 }, 534 535 responseHeaderValue: function(headerName) 536 { 537 return this._headerValue(this.responseHeaders, headerName); 538 }, 539 540 get responseCookies() 541 { 542 if (!this._responseCookies) 543 this._responseCookies = WebInspector.CookieParser.parseSetCookie(this.responseHeaderValue("Set-Cookie")); 544 return this._responseCookies; 545 }, 546 547 get queryParameters() 548 { 549 if (this._parsedQueryParameters) 550 return this._parsedQueryParameters; 551 var queryString = this.url.split("?", 2)[1]; 552 if (!queryString) 553 return; 554 this._parsedQueryParameters = this._parseParameters(queryString); 555 return this._parsedQueryParameters; 556 }, 557 558 get formParameters() 559 { 560 if (this._parsedFormParameters) 561 return this._parsedFormParameters; 562 if (!this.requestFormData) 563 return; 564 var requestContentType = this.requestHeaderValue("Content-Type"); 565 if (!requestContentType || !requestContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i)) 566 return; 567 this._parsedFormParameters = this._parseParameters(this.requestFormData); 568 return this._parsedFormParameters; 569 }, 570 571 _parseParameters: function(queryString) 572 { 573 function parseNameValue(pair) 574 { 575 var parameter = {}; 576 var splitPair = pair.split("=", 2); 577 578 parameter.name = splitPair[0]; 579 if (splitPair.length === 1) 580 parameter.value = ""; 581 else 582 parameter.value = splitPair[1]; 583 return parameter; 584 } 585 return queryString.split("&").map(parseNameValue); 586 }, 587 588 _headerValue: function(headers, headerName) 589 { 590 headerName = headerName.toLowerCase(); 591 for (var header in headers) { 592 if (header.toLowerCase() === headerName) 593 return headers[header]; 594 } 595 }, 596 597 _headersSize: function(headers) 598 { 599 // We should take actual headers size from network stack, when possible, but fall back to 600 // this lousy computation when no headers text is available. 601 var size = 0; 602 for (var header in headers) 603 size += header.length + headers[header].length + 4; // _typical_ overhead per header is ": ".length + "\r\n".length. 604 return size; 605 }, 606 607 get errors() 608 { 609 return this._errors || 0; 610 }, 611 612 set errors(x) 613 { 614 this._errors = x; 615 this.dispatchEventToListeners("errors-warnings-updated"); 616 }, 617 618 get warnings() 619 { 620 return this._warnings || 0; 621 }, 622 623 set warnings(x) 624 { 625 this._warnings = x; 626 this.dispatchEventToListeners("errors-warnings-updated"); 627 }, 628 629 clearErrorsAndWarnings: function() 630 { 631 this._warnings = 0; 632 this._errors = 0; 633 this.dispatchEventToListeners("errors-warnings-updated"); 634 }, 635 636 _mimeTypeIsConsistentWithType: function() 637 { 638 // If status is an error, content is likely to be of an inconsistent type, 639 // as it's going to be an error message. We do not want to emit a warning 640 // for this, though, as this will already be reported as resource loading failure. 641 // Also, if a URL like http://localhost/wiki/load.php?debug=true&lang=en produces text/css and gets reloaded, 642 // it is 304 Not Modified and its guessed mime-type is text/php, which is wrong. 643 // Don't check for mime-types in 304-resources. 644 if (this.statusCode >= 400 || this.statusCode === 304) 645 return true; 646 647 if (typeof this.type === "undefined" 648 || this.type === WebInspector.Resource.Type.Other 649 || this.type === WebInspector.Resource.Type.XHR 650 || this.type === WebInspector.Resource.Type.WebSocket) 651 return true; 652 653 if (!this.mimeType) 654 return true; // Might be not known for cached resources with null responses. 655 656 if (this.mimeType in WebInspector.MIMETypes) 657 return this.type in WebInspector.MIMETypes[this.mimeType]; 658 659 return false; 660 }, 661 662 _checkWarnings: function() 663 { 664 for (var warning in WebInspector.Warnings) 665 this._checkWarning(WebInspector.Warnings[warning]); 666 }, 667 668 _checkWarning: function(warning) 669 { 670 var msg; 671 switch (warning.id) { 672 case WebInspector.Warnings.IncorrectMIMEType.id: 673 if (!this._mimeTypeIsConsistentWithType()) 674 msg = new WebInspector.ConsoleMessage(WebInspector.ConsoleMessage.MessageSource.Other, 675 WebInspector.ConsoleMessage.MessageType.Log, 676 WebInspector.ConsoleMessage.MessageLevel.Warning, 677 -1, 678 this.url, 679 1, 680 String.sprintf(WebInspector.Warnings.IncorrectMIMEType.message, WebInspector.Resource.Type.toUIString(this.type), this.mimeType), 681 null, 682 null); 683 break; 684 } 685 686 if (msg) 687 WebInspector.console.addMessage(msg); 688 }, 689 690 get content() 691 { 692 return this._content; 693 }, 694 695 get contentTimestamp() 696 { 697 return this._contentTimestamp; 698 }, 699 700 setInitialContent: function(content) 701 { 702 this._content = content; 703 }, 704 705 isEditable: function() 706 { 707 if (this._actualResource) 708 return false; 709 var binding = WebInspector.Resource._domainModelBindings[this.type]; 710 return binding && binding.canSetContent(this); 711 }, 712 713 setContent: function(newContent, majorChange, callback) 714 { 715 if (!this.isEditable(this)) { 716 if (callback) 717 callback("Resource is not editable"); 718 return; 719 } 720 var binding = WebInspector.Resource._domainModelBindings[this.type]; 721 binding.setContent(this, newContent, majorChange, callback); 722 }, 723 724 addRevision: function(newContent) 725 { 726 var revision = new WebInspector.ResourceRevision(this, this._content, this._contentTimestamp); 727 this.history.push(revision); 728 729 this._content = newContent; 730 this._contentTimestamp = new Date(); 731 732 this.dispatchEventToListeners(WebInspector.Resource.Events.RevisionAdded, revision); 733 }, 734 735 requestContent: function(callback) 736 { 737 // We do not support content retrieval for WebSockets at the moment. 738 // Since WebSockets are potentially long-living, fail requests immediately 739 // to prevent caller blocking until resource is marked as finished. 740 if (this.type === WebInspector.Resource.Type.WebSocket) { 741 callback(null, null); 742 return; 743 } 744 if (typeof this._content !== "undefined") { 745 callback(this.content, this._contentEncoded); 746 return; 747 } 748 this._pendingContentCallbacks.push(callback); 749 if (this.finished) 750 this._innerRequestContent(); 751 }, 752 753 populateImageSource: function(image) 754 { 755 function onResourceContent() 756 { 757 image.src = this._contentURL(); 758 } 759 760 if (Preferences.useDataURLForResourceImageIcons) 761 this.requestContent(onResourceContent.bind(this)); 762 else 763 image.src = this.url; 764 }, 765 766 isDataURL: function() 767 { 768 return this.url.match(/^data:/i); 769 }, 770 771 _contentURL: function() 772 { 773 const maxDataUrlSize = 1024 * 1024; 774 // If resource content is not available or won't fit a data URL, fall back to using original URL. 775 if (this._content == null || this._content.length > maxDataUrlSize) 776 return this.url; 777 778 return "data:" + this.mimeType + (this._contentEncoded ? ";base64," : ",") + this._content; 779 }, 780 781 _innerRequestContent: function() 782 { 783 if (this._contentRequested) 784 return; 785 this._contentRequested = true; 786 this._contentEncoded = !WebInspector.Resource.Type.isTextType(this.type); 787 788 function onResourceContent(data) 789 { 790 this._content = data; 791 this._originalContent = data; 792 var callbacks = this._pendingContentCallbacks.slice(); 793 for (var i = 0; i < callbacks.length; ++i) 794 callbacks[i](this._content, this._contentEncoded); 795 this._pendingContentCallbacks.length = 0; 796 delete this._contentRequested; 797 } 798 WebInspector.networkManager.requestContent(this, this._contentEncoded, onResourceContent.bind(this)); 799 } 800 } 801 802 WebInspector.Resource.prototype.__proto__ = WebInspector.Object.prototype; 803 804 WebInspector.ResourceRevision = function(resource, content, timestamp) 805 { 806 this._resource = resource; 807 this._content = content; 808 this._timestamp = timestamp; 809 } 810 811 WebInspector.ResourceRevision.prototype = { 812 get resource() 813 { 814 return this._resource; 815 }, 816 817 get timestamp() 818 { 819 return this._timestamp; 820 }, 821 822 get content() 823 { 824 return this._content; 825 }, 826 827 revertToThis: function() 828 { 829 function revert(content) 830 { 831 this._resource.setContent(content, true); 832 } 833 this.requestContent(revert.bind(this)); 834 }, 835 836 requestContent: function(callback) 837 { 838 if (typeof this._content === "string") { 839 callback(this._content); 840 return; 841 } 842 843 // If we are here, this is initial revision. First, look up content fetched over the wire. 844 if (typeof this.resource._originalContent === "string") { 845 this._content = this._resource._originalContent; 846 callback(this._content); 847 return; 848 } 849 850 // If unsuccessful, request the content. 851 function mycallback(content) 852 { 853 this._content = content; 854 callback(content); 855 } 856 WebInspector.networkManager.requestContent(this._resource, false, mycallback.bind(this)); 857 } 858 } 859 860 WebInspector.ResourceDomainModelBinding = function() 861 { 862 } 863 864 WebInspector.ResourceDomainModelBinding.prototype = { 865 canSetContent: function() 866 { 867 // Implemented by the domains. 868 return true; 869 }, 870 871 setContent: function(resource, content, majorChange, callback) 872 { 873 // Implemented by the domains. 874 } 875 } 876