Hook up the initiator info and show it in the Resource details sidebar.
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Resource.js
1 /*
2  * Copyright (C) 2013 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  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.Resource = function(url, mimeType, type, loaderIdentifier, requestIdentifier, requestMethod, requestHeaders, requestData, requestSentTimestamp, initiatorSourceCodeLocation)
27 {
28     WebInspector.SourceCode.call(this);
29
30     console.assert(url);
31
32     if (type in WebInspector.Resource.Type)
33         type = WebInspector.Resource.Type[type];
34
35     this._url = url;
36     this._mimeType = mimeType;
37     this._type = type || WebInspector.Resource.Type.fromMIMEType(mimeType);
38     this._loaderIdentifier = loaderIdentifier || null;
39     this._requestIdentifier = requestIdentifier || null;
40     this._requestMethod = requestMethod || null;
41     this._requestData = requestData || null;
42     this._requestHeaders = requestHeaders || {};
43     this._responseHeaders = {};
44     this._parentFrame = null;
45     this._initiatorSourceCodeLocation = initiatorSourceCodeLocation || null;
46     this._requestSentTimestamp = requestSentTimestamp || NaN;
47     this._responseReceivedTimestamp = NaN;
48     this._lastRedirectReceivedTimestamp = NaN;
49     this._lastDataReceivedTimestamp = NaN;
50     this._finishedOrFailedTimestamp = NaN;
51     this._size = NaN;
52     this._transferSize = NaN;
53     this._cached = false;
54 };
55
56 WebInspector.Object.addConstructorFunctions(WebInspector.Resource);
57
58 WebInspector.Resource.Event = {
59     URLDidChange: "resource-url-did-change",
60     MIMETypeDidChange: "resource-mime-type-did-change",
61     TypeDidChange: "resource-type-did-change",
62     RequestHeadersDidChange: "resource-request-headers-did-change",
63     ResponseReceived: "resource-response-received",
64     LoadingDidFinish: "resource-loading-did-finish",
65     LoadingDidFail: "resource-loading-did-fail",
66     TimestampsDidChange: "resource-timestamps-did-change",
67     SizeDidChange: "resource-size-did-change",
68     TransferSizeDidChange: "resource-transfer-size-did-change",
69     CacheStatusDidChange: "resource-cached-did-change"
70 };
71
72 // Keep these in sync with the "ResourceType" enum defined by the "Page" domain (see WebCore/inspector/Inspector.json).
73 WebInspector.Resource.Type = {
74     Document: "resource-type-document",
75     Stylesheet: "resource-type-stylesheet",
76     Image: "resource-type-image",
77     Font: "resource-type-font",
78     Script: "resource-type-script",
79     XHR: "resource-type-xhr",
80     WebSocket: "resource-type-websocket",
81     Other: "resource-type-other"
82 };
83
84 // This MIME Type map is private, use WebInspector.Resource.Type.fromMIMEType().
85 WebInspector.Resource.Type._mimeTypeMap = {
86     "text/html": WebInspector.Resource.Type.Document,
87     "text/xml": WebInspector.Resource.Type.Document,
88     "text/plain": WebInspector.Resource.Type.Document,
89     "application/xhtml+xml": WebInspector.Resource.Type.Document,
90     "image/svg+xml": WebInspector.Resource.Type.Document,
91
92     "text/css": WebInspector.Resource.Type.Stylesheet,
93     "text/xsl": WebInspector.Resource.Type.Stylesheet,
94     "text/x-less": WebInspector.Resource.Type.Stylesheet,
95     "text/x-sass": WebInspector.Resource.Type.Stylesheet,
96     "text/x-scss": WebInspector.Resource.Type.Stylesheet,
97
98     "application/pdf": WebInspector.Resource.Type.Image,
99
100     "application/x-font-type1": WebInspector.Resource.Type.Font,
101     "application/x-font-ttf": WebInspector.Resource.Type.Font,
102     "application/x-font-woff": WebInspector.Resource.Type.Font,
103     "application/x-truetype-font": WebInspector.Resource.Type.Font,
104
105     "text/javascript": WebInspector.Resource.Type.Script,
106     "text/ecmascript": WebInspector.Resource.Type.Script,
107     "application/javascript": WebInspector.Resource.Type.Script,
108     "application/ecmascript": WebInspector.Resource.Type.Script,
109     "application/x-javascript": WebInspector.Resource.Type.Script,
110     "application/json": WebInspector.Resource.Type.Script,
111     "application/x-json": WebInspector.Resource.Type.Script,
112     "text/x-javascript": WebInspector.Resource.Type.Script,
113     "text/x-json": WebInspector.Resource.Type.Script,
114     "text/javascript1.1": WebInspector.Resource.Type.Script,
115     "text/javascript1.2": WebInspector.Resource.Type.Script,
116     "text/javascript1.3": WebInspector.Resource.Type.Script,
117     "text/jscript": WebInspector.Resource.Type.Script,
118     "text/livescript": WebInspector.Resource.Type.Script,
119     "text/x-livescript": WebInspector.Resource.Type.Script,
120     "text/typescript": WebInspector.Resource.Type.Script,
121     "text/x-clojure": WebInspector.Resource.Type.Script,
122     "text/x-coffeescript": WebInspector.Resource.Type.Script
123 };
124
125 WebInspector.Resource.Type.fromMIMEType = function(mimeType)
126 {
127     if (!mimeType)
128         return WebInspector.Resource.Type.Other;
129
130     mimeType = parseMIMEType(mimeType).type;
131
132     if (mimeType in WebInspector.Resource.Type._mimeTypeMap)
133         return WebInspector.Resource.Type._mimeTypeMap[mimeType];
134
135     if (mimeType.startsWith("image/"))
136         return WebInspector.Resource.Type.Image;
137
138     if (mimeType.startsWith("font/"))
139         return WebInspector.Resource.Type.Font;
140
141     return WebInspector.Resource.Type.Other;
142 };
143
144 WebInspector.Resource.Type.displayName = function(type, plural)
145 {
146     switch(type) {
147     case WebInspector.Resource.Type.Document:
148         if (plural)
149             return WebInspector.UIString("Documents");
150         return WebInspector.UIString("Document");
151     case WebInspector.Resource.Type.Stylesheet:
152         if (plural)
153             return WebInspector.UIString("Stylesheets");
154         return WebInspector.UIString("Stylesheet");
155     case WebInspector.Resource.Type.Image:
156         if (plural)
157             return WebInspector.UIString("Images");
158         return WebInspector.UIString("Image");
159     case WebInspector.Resource.Type.Font:
160         if (plural)
161             return WebInspector.UIString("Fonts");
162         return WebInspector.UIString("Font");
163     case WebInspector.Resource.Type.Script:
164         if (plural)
165             return WebInspector.UIString("Scripts");
166         return WebInspector.UIString("Script");
167     case WebInspector.Resource.Type.XHR:
168         if (plural)
169             return WebInspector.UIString("XHRs");
170         return WebInspector.UIString("XHR");
171     case WebInspector.Resource.Type.WebSocket:
172         if (plural)
173             return WebInspector.UIString("Sockets");
174         return WebInspector.UIString("Socket");
175     case WebInspector.Resource.Type.Other:
176         return WebInspector.UIString("Other");
177     default:
178         console.error("Unknown resource type: ", type);
179         return null;
180     }
181 };
182
183 WebInspector.Resource.prototype = {
184     constructor: WebInspector.Resource,
185
186     // Public
187
188     get url()
189     {
190         return this._url;
191     },
192
193     get urlComponents()
194     {
195         if (!this._urlComponents)
196             this._urlComponents = parseURL(this._url);
197         return this._urlComponents;
198     },
199
200     get displayName()
201     {
202         return WebInspector.displayNameForURL(this._url, this.urlComponents);
203     },
204
205     get initiatorSourceCodeLocation()
206     {
207         return this._initiatorSourceCodeLocation;
208     },
209
210     get type()
211     {
212         return this._type;
213     },
214
215     get mimeType()
216     {
217         return this._mimeType;
218     },
219
220     get mimeTypeComponents()
221     {
222         if (!this._mimeTypeComponents)
223             this._mimeTypeComponents = parseMIMEType(this._mimeType);
224         return this._mimeTypeComponents;
225     },
226
227     get syntheticMIMEType()
228     {
229         // Resources are often transferred with a MIME-type that doesn't match the purpose the
230         // resource was loaded for, which is what WebInspector.Resource.Type represents.
231         // This getter generates a MIME-type, if needed, that matches the resource type.
232
233         // If the type matches the Resource.Type of the MIME-type, then return the actual MIME-type.
234         if (this._type === WebInspector.Resource.Type.fromMIMEType(this._mimeType))
235             return this._mimeType;
236
237         // Return the default MIME-types for the Resource.Type, since the current MIME-type
238         // does not match what is expected for the Resource.Type.
239         switch (this._type) {
240         case WebInspector.Resource.Type.Document:
241             return "text/html";
242         case WebInspector.Resource.Type.Stylesheet:
243             return "text/css";
244         case WebInspector.Resource.Type.Script:
245             return "text/javascript";
246         }
247
248         // Return the actual MIME-type since we don't have a better synthesized one to return.
249         return this._mimeType;
250     },
251
252     get contentURL()
253     {
254         const maximumDataURLSize = 1024 * 1024; // 1 MiB
255
256         // If content is not available or won't fit a data URL, fallback to using original URL.
257         var content = this.content;
258         if (content === null || content.length > maximumDataURLSize)
259             return this._url;
260
261         return "data:" + this.mimeTypeComponents.type + (this.contentIsBase64Encoded ? ";base64," + content : "," + encodeURIComponent(content));
262     },
263
264     isMainResource: function()
265     {
266         return this._parentFrame ? this._parentFrame.mainResource === this : false;
267     },
268
269     get parentFrame()
270     {
271         return this._parentFrame;
272     },
273
274     get loaderIdentifier()
275     {
276         return this._loaderIdentifier;
277     },
278
279     get requestIdentifier()
280     {
281         return this._requestIdentifier;
282     },
283
284     get finished()
285     {
286         return this._finished;
287     },
288
289     get failed()
290     {
291         return this._failed;
292     },
293
294     get canceled()
295     {
296         return this._canceled;
297     },
298
299     get requestMethod()
300     {
301         return this._requestMethod;
302     },
303
304     get requestData()
305     {
306         return this._requestData;
307     },
308
309     get requestDataContentType()
310     {
311         return this._requestHeaders.valueForCaseInsensitiveKey("Content-Type") || null;
312     },
313
314     get requestHeaders()
315     {
316         return this._requestHeaders;
317     },
318
319     get responseHeaders()
320     {
321         return this._responseHeaders;
322     },
323
324     get requestSentTimestamp()
325     {
326         return this._requestSentTimestamp;
327     },
328
329     get lastRedirectReceivedTimestamp()
330     {
331         return this._lastRedirectReceivedTimestamp;
332     },
333
334     get responseReceivedTimestamp()
335     {
336         return this._responseReceivedTimestamp;
337     },
338
339     get lastDataReceivedTimestamp()
340     {
341         return this._lastDataReceivedTimestamp;
342     },
343
344     get finishedOrFailedTimestamp()
345     {
346         return this._finishedOrFailedTimestamp;
347     },
348
349     get firstTimestamp()
350     {
351         return this.requestSentTimestamp || this.lastRedirectReceivedTimestamp || this.responseReceivedTimestamp || this.lastDataReceivedTimestamp || this.finishedOrFailedTimestamp;
352     },
353
354     get lastTimestamp()
355     {
356         return this.finishedOrFailedTimestamp || this.lastDataReceivedTimestamp || this.responseReceivedTimestamp || this.lastRedirectReceivedTimestamp || this.requestSentTimestamp;
357     },
358
359     get duration()
360     {
361         return this._finishedOrFailedTimestamp - this._requestSentTimestamp;
362     },
363
364     get latency()
365     {
366         return this._responseReceivedTimestamp - this._requestSentTimestamp;
367     },
368
369     get receiveDuration()
370     {
371         return this._finishedOrFailedTimestamp - this._responseReceivedTimestamp;
372     },
373
374     get cached()
375     {
376         return this._cached;
377     },
378
379     get statusCode()
380     {
381         return this._statusCode;
382     },
383
384     get statusText()
385     {
386         return this._statusText;
387     },
388
389     get size()
390     {
391         return this._size;
392     },
393
394     get encodedSize()
395     {
396         if (!isNaN(this._transferSize))
397             return this._transferSize;
398
399         // If we did not receive actual transfer size from network
400         // stack, we prefer using Content-Length over resourceSize as
401         // resourceSize may differ from actual transfer size if platform's
402         // network stack performed decoding (e.g. gzip decompression).
403         // The Content-Length, though, is expected to come from raw
404         // response headers and will reflect actual transfer length.
405         // This won't work for chunked content encoding, so fall back to
406         // resourceSize when we don't have Content-Length. This still won't
407         // work for chunks with non-trivial encodings. We need a way to
408         // get actual transfer size from the network stack.
409
410         return Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length") || this._size);
411     },
412
413     get transferSize()
414     {
415         if (this.statusCode === 304) // Not modified
416             return this._responseHeadersSize;
417
418         if (this._cached)
419             return 0;
420
421         return this._responseHeadersSize + this.encodedSize;
422     },
423
424     get compressed()
425     {
426         var contentEncoding = this._responseHeaders.valueForCaseInsensitiveKey("Content-Encoding");
427         return contentEncoding && /\b(?:gzip|deflate)\b/.test(contentEncoding);
428     },
429
430     get scripts()
431     {
432         return this._scripts || [];
433     },
434
435     scriptForLocation: function(sourceCodeLocation)
436     {
437         console.assert(!(this instanceof WebInspector.SourceMapResource));
438         console.assert(sourceCodeLocation.sourceCode === this, "SourceCodeLocation must be in this Resource");
439         if (sourceCodeLocation.sourceCode !== this)
440             return null;
441
442         var lineNumber = sourceCodeLocation.lineNumber;
443         var columnNumber = sourceCodeLocation.columnNumber;
444         for (var i = 0; i < this._scripts.length; ++i) {
445             var script = this._scripts[i];
446             if (script.range.startLine <= lineNumber && script.range.endLine >= lineNumber) {
447                 if (script.range.startLine === lineNumber && columnNumber < script.range.startColumn)
448                     continue;
449                 if (script.range.endLine === lineNumber && columnNumber > script.range.endColumn)
450                     continue;
451                 return script;
452             }
453         }
454
455         return null;
456     },
457
458     updateForRedirectResponse: function(url, requestHeaders, timestamp)
459     {
460         console.assert(!this._finished);
461         console.assert(!this._failed);
462         console.assert(!this._canceled);
463
464         var oldURL = this._url;
465
466         this._url = url;
467         this._requestHeaders = requestHeaders || {};
468         this._lastRedirectReceivedTimestamp = timestamp || NaN;
469
470         if (oldURL !== url) {
471             // Delete the URL components so the URL is re-parsed the next time it is requested.
472             delete this._urlComponents;
473
474             this.dispatchEventToListeners(WebInspector.Resource.Event.URLDidChange, {oldURL: oldURL});
475         }
476
477         this.dispatchEventToListeners(WebInspector.Resource.Event.RequestHeadersDidChange);
478         this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange);
479     },
480
481     updateForResponse: function(url, mimeType, type, responseHeaders, statusCode, statusText, timestamp)
482     {
483         console.assert(!this._finished);
484         console.assert(!this._failed);
485         console.assert(!this._canceled);
486
487         var oldURL = this._url;
488         var oldMIMEType = this._mimeType;
489         var oldType = this._type;
490
491         if (type in WebInspector.Resource.Type)
492             type = WebInspector.Resource.Type[type];
493
494         this._url = url;
495         this._mimeType = mimeType;
496         this._type = type || WebInspector.Resource.Type.fromMIMEType(mimeType);
497         this._statusCode = statusCode;
498         this._statusText = statusText;
499         this._responseHeaders = responseHeaders || {};
500         this._responseReceivedTimestamp = timestamp || NaN;
501
502         this._responseHeadersSize = String(this._statusCode).length + this._statusText.length + 12; // Extra length is for "HTTP/1.1 ", " ", and "\r\n".
503         for (var name in this._responseHeaders)
504             this._responseHeadersSize += name.length + this._responseHeaders[name].length + 4; // Extra length is for ": ", and "\r\n".
505
506         if (statusCode === 304 && !this._cached)
507             this.markAsCached();
508
509         if (oldURL !== url) {
510             // Delete the URL components so the URL is re-parsed the next time it is requested.
511             delete this._urlComponents;
512
513             this.dispatchEventToListeners(WebInspector.Resource.Event.URLDidChange, {oldURL: oldURL});
514         }
515
516         if (oldMIMEType !== mimeType) {
517             // Delete the MIME-type components so the MIME-type is re-parsed the next time it is requested.
518             delete this._mimeTypeComponents;
519
520             this.dispatchEventToListeners(WebInspector.Resource.Event.MIMETypeDidChange, {oldMIMEType: oldMIMEType});
521         }
522
523         if (oldType !== type)
524             this.dispatchEventToListeners(WebInspector.Resource.Event.TypeDidChange, {oldType: oldType});
525
526         console.assert(isNaN(this._size));
527         console.assert(isNaN(this._transferSize));
528
529         // The transferSize becomes 0 when status is 304 or Content-Length is available, so
530         // notify listeners of that change.
531         if (statusCode === 304 || this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
532             this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange);
533
534         this.dispatchEventToListeners(WebInspector.Resource.Event.ResponseReceived);
535         this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange);
536     },
537
538     canRequestContentFromBackend: function()
539     {
540         return this._finished;
541     },
542
543     requestContentFromBackend: function(callback)
544     {
545         // If we have the requestIdentifier we can get the actual response for this specific resource.
546         // Otherwise the content will be cached resource data, which might not exist anymore.
547         if (this._requestIdentifier) {
548             NetworkAgent.getResponseBody(this._requestIdentifier, callback);
549             return true;
550         }
551
552         if (this._parentFrame) {
553             PageAgent.getResourceContent(this._parentFrame.id, this._url, callback);
554             return true;
555         }
556
557         // There is no request identifier or frame to request content from. Return false to cause the
558         // pending callbacks to get null content.
559         return false;
560     },
561
562     increaseSize: function(dataLength, timestamp)
563     {
564         console.assert(dataLength >= 0);
565
566         if (isNaN(this._size))
567             this._size = 0;
568
569         var previousSize = this._size;
570
571         this._size += dataLength;
572
573         this._lastDataReceivedTimestamp = timestamp || NaN;
574
575         this.dispatchEventToListeners(WebInspector.Resource.Event.SizeDidChange, {previousSize: previousSize});
576
577         // The transferSize is based off of size when status is not 304 or Content-Length is missing.
578         if (isNaN(this._transferSize) && this._statusCode !== 304 && !this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
579             this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange);
580     },
581
582     increaseTransferSize: function(encodedDataLength)
583     {
584         console.assert(encodedDataLength >= 0);
585
586         if (isNaN(this._transferSize))
587             this._transferSize = 0;
588         this._transferSize += encodedDataLength;
589
590         this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange);
591     },
592
593     markAsCached: function()
594     {
595         this._cached = true;
596
597         this.dispatchEventToListeners(WebInspector.Resource.Event.CacheStatusDidChange);
598
599         // The transferSize is starts returning 0 when cached is true, unless status is 304.
600         if (this._statusCode !== 304)
601             this.dispatchEventToListeners(WebInspector.Resource.Event.TransferSizeDidChange);
602     },
603
604     markAsFinished: function(timestamp)
605     {
606         console.assert(!this._failed);
607         console.assert(!this._canceled);
608
609         this._finished = true;
610         this._finishedOrFailedTimestamp = timestamp || NaN;
611
612         this.dispatchEventToListeners(WebInspector.Resource.Event.LoadingDidFinish);
613         this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange);
614
615         if (this.canRequestContentFromBackend())
616             this.requestContentFromBackendIfNeeded();
617     },
618
619     markAsFailed: function(canceled, timestamp)
620     {
621         console.assert(!this._finished);
622
623         this._failed = true;
624         this._canceled = canceled;
625         this._finishedOrFailedTimestamp = timestamp || NaN;
626
627         this.dispatchEventToListeners(WebInspector.Resource.Event.LoadingDidFail);
628         this.dispatchEventToListeners(WebInspector.Resource.Event.TimestampsDidChange);
629
630         // Force the content requests to be serviced. They will get null as the content.
631         this.servicePendingContentRequests(true);
632     },
633
634     revertMarkAsFinished: function(timestamp)
635     {
636         console.assert(!this._failed);
637         console.assert(!this._canceled);
638         console.assert(this._finished);
639
640         this._finished = false;
641         this._finishedOrFailedTimestamp = NaN;
642     },
643
644     getImageSize: function(callback)
645     {
646         // Throw an error in the case this resource is not an image.
647         if (this.type !== WebInspector.Resource.Type.Image)
648             throw "Resource is not an image.";
649
650         // See if we've already computed and cached the image size,
651         // in which case we can provide them directly.
652         if (this._imageSize) {
653             callback(this._imageSize);
654             return;
655         }
656
657         // Event handler for the image "load" event.
658         function imageDidLoad()
659         {
660             // Cache the image metrics.
661             this._imageSize = {
662                 width: image.width,
663                 height: image.height
664             };
665             
666             callback(this._imageSize);
667         };
668
669         // Create an <img> element that we'll use to load the image resource
670         // so that we can query its intrinsic size.
671         var image = new Image;
672         image.addEventListener("load", imageDidLoad.bind(this), false);
673
674         // Set the image source once we've obtained the base64-encoded URL for it.
675         this.requestContent(function() {
676             image.src = this.contentURL;
677         }.bind(this));
678     },
679
680     associateWithScript: function(script)
681     {
682         if (!this._scripts)
683             this._scripts = []
684
685         this._scripts.push(script);
686
687         // COMPATIBILITY (iOS 6): Resources did not know their type until a response
688         // was received. We can set the Resource type to be Script here.
689         if (this._type === WebInspector.Resource.Type.Other) {
690             var oldType = this._type;
691             this._type = WebInspector.Resource.Type.Script;
692             this.dispatchEventToListeners(WebInspector.Resource.Event.TypeDidChange, {oldType: oldType});
693         }
694     }
695 };
696
697 WebInspector.Resource.prototype.__proto__ = WebInspector.SourceCode.prototype;