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 // See http://www.softwareishard.com/blog/har-12-spec/ 32 // for HAR specification. 33 34 // FIXME: Some fields are not yet supported due to back-end limitations. 35 // See https://bugs.webkit.org/show_bug.cgi?id=58127 for details. 36 37 /** 38 * @constructor 39 * @param {WebInspector.NetworkRequest} request 40 */ 41 WebInspector.HAREntry = function(request) 42 { 43 this._request = request; 44 } 45 46 WebInspector.HAREntry.prototype = { 47 /** 48 * @return {Object} 49 */ 50 build: function() 51 { 52 var entry = { 53 startedDateTime: new Date(this._request.startTime * 1000), 54 time: WebInspector.HAREntry._toMilliseconds(this._request.duration), 55 request: this._buildRequest(), 56 response: this._buildResponse(), 57 cache: { }, // Not supported yet. 58 timings: this._buildTimings() 59 }; 60 var page = WebInspector.networkLog.pageLoadForRequest(this._request); 61 if (page) 62 entry.pageref = "page_" + page.id; 63 return entry; 64 }, 65 66 /** 67 * @return {Object} 68 */ 69 _buildRequest: function() 70 { 71 var res = { 72 method: this._request.requestMethod, 73 url: this._buildRequestURL(this._request.url), 74 httpVersion: this._request.requestHttpVersion, 75 headers: this._request.requestHeaders, 76 queryString: this._buildParameters(this._request.queryParameters || []), 77 cookies: this._buildCookies(this._request.requestCookies || []), 78 headersSize: this._request.requestHeadersSize, 79 bodySize: this.requestBodySize 80 }; 81 if (this._request.requestFormData) 82 res.postData = this._buildPostData(); 83 84 return res; 85 }, 86 87 /** 88 * @return {Object} 89 */ 90 _buildResponse: function() 91 { 92 return { 93 status: this._request.statusCode, 94 statusText: this._request.statusText, 95 httpVersion: this._request.responseHttpVersion, 96 headers: this._request.responseHeaders, 97 cookies: this._buildCookies(this._request.responseCookies || []), 98 content: this._buildContent(), 99 redirectURL: this._request.responseHeaderValue("Location") || "", 100 headersSize: this._request.responseHeadersSize, 101 bodySize: this.responseBodySize 102 }; 103 }, 104 105 /** 106 * @return {Object} 107 */ 108 _buildContent: function() 109 { 110 var content = { 111 size: this._request.resourceSize, 112 mimeType: this._request.mimeType, 113 // text: this._request.content // TODO: pull out into a boolean flag, as content can be huge (and needs to be requested with an async call) 114 }; 115 var compression = this.responseCompression; 116 if (typeof compression === "number") 117 content.compression = compression; 118 return content; 119 }, 120 121 /** 122 * @return {Object} 123 */ 124 _buildTimings: function() 125 { 126 var waitForConnection = this._interval("connectStart", "connectEnd"); 127 var blocked = 0; 128 var connect = -1; 129 130 if (this._request.connectionReused) 131 blocked = waitForConnection; 132 else 133 connect = waitForConnection; 134 135 return { 136 blocked: blocked, 137 dns: this._interval("dnsStart", "dnsEnd"), 138 connect: connect, 139 send: this._interval("sendStart", "sendEnd"), 140 wait: this._interval("sendEnd", "receiveHeadersEnd"), 141 receive: WebInspector.HAREntry._toMilliseconds(this._request.receiveDuration), 142 ssl: this._interval("sslStart", "sslEnd") 143 }; 144 }, 145 146 /** 147 * @return {Object} 148 */ 149 _buildPostData: function() 150 { 151 var res = { 152 mimeType: this._request.requestHeaderValue("Content-Type"), 153 text: this._request.requestFormData 154 }; 155 if (this._request.formParameters) 156 res.params = this._buildParameters(this._request.formParameters); 157 return res; 158 }, 159 160 /** 161 * @param {Array.<Object>} parameters 162 * @return {Array.<Object>} 163 */ 164 _buildParameters: function(parameters) 165 { 166 return parameters.slice(); 167 }, 168 169 /** 170 * @param {string} url 171 * @return {string} 172 */ 173 _buildRequestURL: function(url) 174 { 175 return url.split("#", 2)[0]; 176 }, 177 178 /** 179 * @param {Array.<WebInspector.Cookie>} cookies 180 * @return {Array.<Object>} 181 */ 182 _buildCookies: function(cookies) 183 { 184 return cookies.map(this._buildCookie.bind(this)); 185 }, 186 187 /** 188 * @param {WebInspector.Cookie} cookie 189 * @return {Object} 190 */ 191 _buildCookie: function(cookie) 192 { 193 return { 194 name: cookie.name(), 195 value: cookie.value(), 196 path: cookie.path(), 197 domain: cookie.domain(), 198 expires: cookie.expiresDate(new Date(this._request.startTime * 1000)), 199 httpOnly: cookie.httpOnly(), 200 secure: cookie.secure() 201 }; 202 }, 203 204 /** 205 * @param {string} start 206 * @param {string} end 207 * @return {number} 208 */ 209 _interval: function(start, end) 210 { 211 var timing = this._request.timing; 212 if (!timing) 213 return -1; 214 var startTime = timing[start]; 215 return typeof startTime !== "number" || startTime === -1 ? -1 : Math.round(timing[end] - startTime); 216 }, 217 218 /** 219 * @return {number} 220 */ 221 get requestBodySize() 222 { 223 return !this._request.requestFormData ? 0 : this._request.requestFormData.length; 224 }, 225 226 /** 227 * @return {number} 228 */ 229 get responseBodySize() 230 { 231 if (this._request.cached || this._request.statusCode === 304) 232 return 0; 233 return this._request.transferSize - this._request.responseHeadersSize; 234 }, 235 236 /** 237 * @return {number|undefined} 238 */ 239 get responseCompression() 240 { 241 if (this._request.cached || this._request.statusCode === 304 || this._request.statusCode === 206) 242 return; 243 return this._request.resourceSize - this.responseBodySize; 244 } 245 } 246 247 /** 248 * @param {number} time 249 * @return {number} 250 */ 251 WebInspector.HAREntry._toMilliseconds = function(time) 252 { 253 return time === -1 ? -1 : Math.round(time * 1000); 254 } 255 256 /** 257 * @constructor 258 * @param {Array.<WebInspector.NetworkRequest>} requests 259 */ 260 WebInspector.HARLog = function(requests) 261 { 262 this._requests = requests; 263 } 264 265 WebInspector.HARLog.prototype = { 266 /** 267 * @return {Object} 268 */ 269 build: function() 270 { 271 return { 272 version: "1.2", 273 creator: this._creator(), 274 pages: this._buildPages(), 275 entries: this._requests.map(this._convertResource.bind(this)) 276 } 277 }, 278 279 _creator: function() 280 { 281 var webKitVersion = /AppleWebKit\/([^ ]+)/.exec(window.navigator.userAgent); 282 283 return { 284 name: "WebInspector", 285 version: webKitVersion ? webKitVersion[1] : "n/a" 286 }; 287 }, 288 289 /** 290 * @return {Array} 291 */ 292 _buildPages: function() 293 { 294 var seenIdentifiers = {}; 295 var pages = []; 296 for (var i = 0; i < this._requests.length; ++i) { 297 var page = WebInspector.networkLog.pageLoadForRequest(this._requests[i]); 298 if (!page || seenIdentifiers[page.id]) 299 continue; 300 seenIdentifiers[page.id] = true; 301 pages.push(this._convertPage(page)); 302 } 303 return pages; 304 }, 305 306 /** 307 * @param {WebInspector.PageLoad} page 308 * @return {Object} 309 */ 310 _convertPage: function(page) 311 { 312 return { 313 startedDateTime: new Date(page.startTime * 1000), 314 id: "page_" + page.id, 315 title: page.url, // We don't have actual page title here. URL is probably better than nothing. 316 pageTimings: { 317 onContentLoad: this._pageEventTime(page, page.contentLoadTime), 318 onLoad: this._pageEventTime(page, page.loadTime) 319 } 320 } 321 }, 322 323 /** 324 * @param {WebInspector.NetworkRequest} request 325 * @return {Object} 326 */ 327 _convertResource: function(request) 328 { 329 return (new WebInspector.HAREntry(request)).build(); 330 }, 331 332 /** 333 * @param {WebInspector.PageLoad} page 334 * @param {number} time 335 * @return {number} 336 */ 337 _pageEventTime: function(page, time) 338 { 339 var startTime = page.startTime; 340 if (time === -1 || startTime === -1) 341 return -1; 342 return WebInspector.HAREntry._toMilliseconds(time - startTime); 343 } 344 } 345 346 /** 347 * @constructor 348 */ 349 WebInspector.HARWriter = function() 350 { 351 } 352 353 WebInspector.HARWriter.prototype = { 354 /** 355 * @param {WebInspector.OutputStream} stream 356 * @param {Array.<WebInspector.NetworkRequest>} requests 357 * @param {WebInspector.Progress} progress 358 */ 359 write: function(stream, requests, progress) 360 { 361 this._stream = stream; 362 this._harLog = (new WebInspector.HARLog(requests)).build(); 363 this._pendingRequests = 1; // Guard against completing resource transfer before all requests are made. 364 var entries = this._harLog.entries; 365 for (var i = 0; i < entries.length; ++i) { 366 var content = requests[i].content; 367 if (typeof content === "undefined" && requests[i].finished) { 368 ++this._pendingRequests; 369 requests[i].requestContent(this._onContentAvailable.bind(this, entries[i])); 370 } else if (content !== null) 371 entries[i].response.content.text = content; 372 } 373 var compositeProgress = new WebInspector.CompositeProgress(progress); 374 this._writeProgress = compositeProgress.createSubProgress(); 375 if (--this._pendingRequests) { 376 this._requestsProgress = compositeProgress.createSubProgress(); 377 this._requestsProgress.setTitle(WebInspector.UIString("Collecting content")); 378 this._requestsProgress.setTotalWork(this._pendingRequests); 379 } else 380 this._beginWrite(); 381 }, 382 383 /** 384 * @param {Object} entry 385 * @param {string|null} content 386 * @param {boolean} contentEncoded 387 * @param {string=} mimeType 388 */ 389 _onContentAvailable: function(entry, content, contentEncoded, mimeType) 390 { 391 if (content !== null) 392 entry.response.content.text = content; 393 if (this._requestsProgress) 394 this._requestsProgress.worked(); 395 if (!--this._pendingRequests) { 396 this._requestsProgress.done(); 397 this._beginWrite(); 398 } 399 }, 400 401 _beginWrite: function() 402 { 403 const jsonIndent = 2; 404 this._text = JSON.stringify({log: this._harLog}, null, jsonIndent); 405 this._writeProgress.setTitle(WebInspector.UIString("Writing file")); 406 this._writeProgress.setTotalWork(this._text.length); 407 this._bytesWritten = 0; 408 this._writeNextChunk(this._stream); 409 }, 410 411 /** 412 * @param {WebInspector.OutputStream} stream 413 * @param {string=} error 414 */ 415 _writeNextChunk: function(stream, error) 416 { 417 if (this._bytesWritten >= this._text.length || error) { 418 stream.close(); 419 this._writeProgress.done(); 420 return; 421 } 422 const chunkSize = 100000; 423 var text = this._text.substring(this._bytesWritten, this._bytesWritten + chunkSize); 424 this._bytesWritten += text.length; 425 stream.write(text, this._writeNextChunk.bind(this)); 426 this._writeProgress.setWorked(this._bytesWritten); 427 } 428 } 429