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