Fix an assertion failure seen on the layout tests, and when closing the window after...
[WebKit-https.git] / WebKit / mac / Plugins / WebBaseNetscapePluginStream.mm
1 /*
2  * Copyright (C) 2005, 2006, 2007 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
29 #ifndef __LP64__
30 #import "WebBaseNetscapePluginStream.h"
31
32 #import "WebBaseNetscapePluginView.h"
33 #import "WebKitErrorsPrivate.h"
34 #import "WebKitLogging.h"
35 #import "WebNSObjectExtras.h"
36 #import "WebNSURLExtras.h"
37 #import "WebNetscapePluginPackage.h"
38 #import <Foundation/NSURLResponse.h>
39 #import <WebCore/WebCoreObjCExtras.h>
40 #import <WebKitSystemInterface.h>
41 #import <wtf/HashMap.h>
42
43 #define WEB_REASON_NONE -1
44
45 static NSString *CarbonPathFromPOSIXPath(NSString *posixPath);
46
47 typedef HashMap<NPStream*, NPP> StreamMap;
48 static StreamMap& streams()
49 {
50     static StreamMap staticStreams;
51     return staticStreams;
52 }
53
54 @implementation WebBaseNetscapePluginStream
55
56 #ifndef BUILDING_ON_TIGER
57 + (void)initialize
58 {
59     WebCoreObjCFinalizeOnMainThread(self);
60 }
61 #endif
62
63 + (NPP)ownerForStream:(NPStream *)stream
64 {
65     return streams().get(stream);
66 }
67
68 + (NPReason)reasonForError:(NSError *)error
69 {
70     if (error == nil) {
71         return NPRES_DONE;
72     }
73     if ([[error domain] isEqualToString:NSURLErrorDomain] && [error code] == NSURLErrorCancelled) {
74         return NPRES_USER_BREAK;
75     }
76     return NPRES_NETWORK_ERR;
77 }
78
79 - (NSError *)_pluginCancelledConnectionError
80 {
81     return [[[NSError alloc] _initWithPluginErrorCode:WebKitErrorPlugInCancelledConnection
82                                            contentURL:responseURL != nil ? responseURL : requestURL
83                                         pluginPageURL:nil
84                                            pluginName:[[pluginView pluginPackage] name]
85                                              MIMEType:MIMEType] autorelease];
86 }
87
88 - (NSError *)errorForReason:(NPReason)theReason
89 {
90     if (theReason == NPRES_DONE) {
91         return nil;
92     }
93     if (theReason == NPRES_USER_BREAK) {
94         return [NSError _webKitErrorWithDomain:NSURLErrorDomain
95                                           code:NSURLErrorCancelled 
96                                            URL:responseURL != nil ? responseURL : requestURL];
97     }
98     return [self _pluginCancelledConnectionError];
99 }
100
101 - (id)initWithRequestURL:(NSURL *)theRequestURL
102                   plugin:(NPP)thePlugin
103               notifyData:(void *)theNotifyData
104         sendNotification:(BOOL)flag
105 {
106     [super init];
107  
108     // Temporarily set isTerminated to YES to avoid assertion failure in dealloc in case we are released in this method.
109     isTerminated = YES;
110
111     if (theRequestURL == nil || thePlugin == NULL) {
112         [self release];
113         return nil;
114     }
115     
116     [self setRequestURL:theRequestURL];
117     [self setPlugin:thePlugin];
118     notifyData = theNotifyData;
119     sendNotification = flag;
120     fileDescriptor = -1;
121
122     streams().add(&stream, thePlugin);
123     
124     isTerminated = NO;
125     
126     return self;
127 }
128
129 - (void)dealloc
130 {
131     ASSERT(!plugin);
132     ASSERT(isTerminated);
133     ASSERT(stream.ndata == nil);
134
135     // The stream file should have been deleted, and the path freed, in -_destroyStream
136     ASSERT(!path);
137     ASSERT(fileDescriptor == -1);
138
139     [requestURL release];
140     [responseURL release];
141     [MIMEType release];
142     [pluginView release];
143     [deliveryData release];
144     
145     free((void *)stream.url);
146     free(path);
147     free(headers);
148
149     streams().remove(&stream);
150
151     [super dealloc];
152 }
153
154 - (void)finalize
155 {
156     ASSERT_MAIN_THREAD();
157     ASSERT(isTerminated);
158     ASSERT(stream.ndata == nil);
159
160     // The stream file should have been deleted, and the path freed, in -_destroyStream
161     ASSERT(!path);
162     ASSERT(fileDescriptor == -1);
163
164     free((void *)stream.url);
165     free(path);
166     free(headers);
167
168     streams().remove(&stream);
169
170     [super finalize];
171 }
172
173 - (uint16)transferMode
174 {
175     return transferMode;
176 }
177
178 - (NPP)plugin
179 {
180     return plugin;
181 }
182
183 - (void)setRequestURL:(NSURL *)theRequestURL
184 {
185     [theRequestURL retain];
186     [requestURL release];
187     requestURL = theRequestURL;
188 }
189
190 - (void)setResponseURL:(NSURL *)theResponseURL
191 {
192     [theResponseURL retain];
193     [responseURL release];
194     responseURL = theResponseURL;
195 }
196
197 - (void)setPlugin:(NPP)thePlugin
198 {
199     if (thePlugin) {
200         plugin = thePlugin;
201         pluginView = [(WebBaseNetscapePluginView *)plugin->ndata retain];
202         WebNetscapePluginPackage *pluginPackage = [pluginView pluginPackage];
203         NPP_NewStream = [pluginPackage NPP_NewStream];
204         NPP_WriteReady = [pluginPackage NPP_WriteReady];
205         NPP_Write = [pluginPackage NPP_Write];
206         NPP_StreamAsFile = [pluginPackage NPP_StreamAsFile];
207         NPP_DestroyStream = [pluginPackage NPP_DestroyStream];
208         NPP_URLNotify = [pluginPackage NPP_URLNotify];
209     } else {
210         WebBaseNetscapePluginView *view = pluginView;
211
212         plugin = NULL;
213         NPP_NewStream = NULL;
214         NPP_WriteReady = NULL;
215         NPP_Write = NULL;
216         NPP_StreamAsFile = NULL;
217         NPP_DestroyStream = NULL;
218         NPP_URLNotify = NULL;
219         pluginView = nil;
220
221         [view disconnectStream:self];
222         [view release];
223     }
224 }
225
226 - (void)setMIMEType:(NSString *)theMIMEType
227 {
228     [theMIMEType retain];
229     [MIMEType release];
230     MIMEType = theMIMEType;
231 }
232
233 - (void)startStreamResponseURL:(NSURL *)URL
234          expectedContentLength:(long long)expectedContentLength
235               lastModifiedDate:(NSDate *)lastModifiedDate
236                       MIMEType:(NSString *)theMIMEType
237                        headers:(NSData *)theHeaders
238 {
239     ASSERT(!isTerminated);
240     
241     [self setResponseURL:URL];
242     [self setMIMEType:theMIMEType];
243     
244     free((void *)stream.url);
245     stream.url = strdup([responseURL _web_URLCString]);
246
247     stream.ndata = self;
248     stream.end = expectedContentLength > 0 ? (uint32)expectedContentLength : 0;
249     stream.lastmodified = (uint32)[lastModifiedDate timeIntervalSince1970];
250     stream.notifyData = notifyData;
251
252     if (theHeaders) {
253         unsigned len = [theHeaders length];
254         headers = (char*) malloc(len + 1);
255         [theHeaders getBytes:headers];
256         headers[len] = 0;
257         stream.headers = headers;
258     }
259     
260     transferMode = NP_NORMAL;
261     offset = 0;
262     reason = WEB_REASON_NONE;
263     // FIXME: If WebNetscapePluginStream called our initializer we wouldn't have to do this here.
264     fileDescriptor = -1;
265
266     // FIXME: Need a way to check if stream is seekable
267
268     WebBaseNetscapePluginView *pv = pluginView;
269     [pv willCallPlugInFunction];
270     NPError npErr = NPP_NewStream(plugin, (char *)[MIMEType UTF8String], &stream, NO, &transferMode);
271     [pv didCallPlugInFunction];
272     LOG(Plugins, "NPP_NewStream URL=%@ MIME=%@ error=%d", responseURL, MIMEType, npErr);
273
274     if (npErr != NPERR_NO_ERROR) {
275         LOG_ERROR("NPP_NewStream failed with error: %d responseURL: %@", npErr, responseURL);
276         // Calling cancelLoadWithError: cancels the load, but doesn't call NPP_DestroyStream.
277         [self cancelLoadWithError:[self _pluginCancelledConnectionError]];
278         return;
279     }
280
281     switch (transferMode) {
282         case NP_NORMAL:
283             LOG(Plugins, "Stream type: NP_NORMAL");
284             break;
285         case NP_ASFILEONLY:
286             LOG(Plugins, "Stream type: NP_ASFILEONLY");
287             break;
288         case NP_ASFILE:
289             LOG(Plugins, "Stream type: NP_ASFILE");
290             break;
291         case NP_SEEK:
292             LOG_ERROR("Stream type: NP_SEEK not yet supported");
293             [self cancelLoadAndDestroyStreamWithError:[self _pluginCancelledConnectionError]];
294             break;
295         default:
296             LOG_ERROR("unknown stream type");
297     }
298 }
299
300 - (void)startStreamWithResponse:(NSURLResponse *)r
301 {
302     NSMutableData *theHeaders = nil;
303     long long expectedContentLength = [r expectedContentLength];
304
305     if ([r isKindOfClass:[NSHTTPURLResponse class]]) {
306         NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)r;
307         theHeaders = [NSMutableData dataWithCapacity:1024];
308         
309         // FIXME: it would be nice to be able to get the raw HTTP header block.
310         // This includes the HTTP version, the real status text,
311         // all headers in their original order and including duplicates,
312         // and all original bytes verbatim, rather than sent through Unicode translation.
313         // Unfortunately NSHTTPURLResponse doesn't provide access at that low a level.
314         
315         [theHeaders appendBytes:"HTTP " length:5];
316         char statusStr[10];
317         long statusCode = [httpResponse statusCode];
318         snprintf(statusStr, sizeof(statusStr), "%ld", statusCode);
319         [theHeaders appendBytes:statusStr length:strlen(statusStr)];
320         [theHeaders appendBytes:" OK\n" length:4];
321
322         // HACK: pass the headers through as UTF-8.
323         // This is not the intended behavior; we're supposed to pass original bytes verbatim.
324         // But we don't have the original bytes, we have NSStrings built by the URL loading system.
325         // It hopefully shouldn't matter, since RFC2616/RFC822 require ASCII-only headers,
326         // but surely someone out there is using non-ASCII characters, and hopefully UTF-8 is adequate here.
327         // It seems better than NSASCIIStringEncoding, which will lose information if non-ASCII is used.
328
329         NSDictionary *headerDict = [httpResponse allHeaderFields];
330         NSArray *keys = [[headerDict allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
331         NSEnumerator *i = [keys objectEnumerator];
332         NSString *k;
333         while ((k = [i nextObject]) != nil) {
334             NSString *v = [headerDict objectForKey:k];
335             [theHeaders appendData:[k dataUsingEncoding:NSUTF8StringEncoding]];
336             [theHeaders appendBytes:": " length:2];
337             [theHeaders appendData:[v dataUsingEncoding:NSUTF8StringEncoding]];
338             [theHeaders appendBytes:"\n" length:1];
339         }
340
341         // If the content is encoded (most likely compressed), then don't send its length to the plugin,
342         // which is only interested in the decoded length, not yet known at the moment.
343         // <rdar://problem/4470599> tracks a request for -[NSURLResponse expectedContentLength] to incorporate this logic.
344         NSString *contentEncoding = (NSString *)[[(NSHTTPURLResponse *)r allHeaderFields] objectForKey:@"Content-Encoding"];
345         if (contentEncoding && ![contentEncoding isEqualToString:@"identity"])
346             expectedContentLength = -1;
347
348         // startStreamResponseURL:... will null-terminate.
349     }
350
351     [self startStreamResponseURL:[r URL]
352            expectedContentLength:expectedContentLength
353                 lastModifiedDate:WKGetNSURLResponseLastModifiedDate(r)
354                         MIMEType:[r MIMEType]
355                          headers:theHeaders];
356 }
357
358 - (void)_destroyStream
359 {
360     if (isTerminated)
361         return;
362
363     [self retain];
364
365     ASSERT(reason != WEB_REASON_NONE);
366     ASSERT([deliveryData length] == 0);
367     
368     [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_deliverData) object:nil];
369
370     if (stream.ndata != nil) {
371         if (reason == NPRES_DONE && (transferMode == NP_ASFILE || transferMode == NP_ASFILEONLY)) {
372             ASSERT(fileDescriptor == -1);
373             ASSERT(path != NULL);
374             NSString *carbonPath = CarbonPathFromPOSIXPath(path);
375             ASSERT(carbonPath != NULL);
376             WebBaseNetscapePluginView *pv = pluginView;
377             [pv willCallPlugInFunction];
378             NPP_StreamAsFile(plugin, &stream, [carbonPath fileSystemRepresentation]);
379             [pv didCallPlugInFunction];
380             LOG(Plugins, "NPP_StreamAsFile responseURL=%@ path=%s", responseURL, carbonPath);
381         }
382
383         if (path) {
384             // Delete the file after calling NPP_StreamAsFile(), instead of in -dealloc/-finalize.  It should be OK
385             // to delete the file here -- NPP_StreamAsFile() is always called immediately before NPP_DestroyStream()
386             // (the stream destruction function), so there can be no expectation that a plugin will read the stream
387             // file asynchronously after NPP_StreamAsFile() is called.
388             unlink([path fileSystemRepresentation]);
389             [path release];
390             path = nil;
391
392             if (isTerminated)
393                 goto exit;
394         }
395
396         if (fileDescriptor != -1) {
397             // The file may still be open if we are destroying the stream before it completed loading.
398             close(fileDescriptor);
399             fileDescriptor = -1;
400         }
401
402         NPError npErr;
403         WebBaseNetscapePluginView *pv = pluginView;
404         [pv willCallPlugInFunction];
405         npErr = NPP_DestroyStream(plugin, &stream, reason);
406         [pv didCallPlugInFunction];
407         LOG(Plugins, "NPP_DestroyStream responseURL=%@ error=%d", responseURL, npErr);
408
409         free(headers);
410         headers = NULL;
411         stream.headers = NULL;
412
413         stream.ndata = nil;
414
415         if (isTerminated)
416             goto exit;
417     }
418
419     if (sendNotification) {
420         // NPP_URLNotify expects the request URL, not the response URL.
421         WebBaseNetscapePluginView *pv = pluginView;
422         [pv willCallPlugInFunction];
423         NPP_URLNotify(plugin, [requestURL _web_URLCString], reason, notifyData);
424         [pv didCallPlugInFunction];
425         LOG(Plugins, "NPP_URLNotify requestURL=%@ reason=%d", requestURL, reason);
426     }
427
428     isTerminated = YES;
429
430     [self setPlugin:NULL];
431
432 exit:
433     [self release];
434 }
435
436 - (void)_destroyStreamWithReason:(NPReason)theReason
437 {
438     reason = theReason;
439     if (reason != NPRES_DONE) {
440         // Stop any pending data from being streamed.
441         [deliveryData setLength:0];
442     } else if ([deliveryData length] > 0) {
443         // There is more data to be streamed, don't destroy the stream now.
444         return;
445     }
446     [self _destroyStream];
447     ASSERT(stream.ndata == nil);
448 }
449
450 - (void)cancelLoadWithError:(NSError *)error
451 {
452     // Overridden by subclasses.
453     ASSERT_NOT_REACHED();
454 }
455
456 - (void)destroyStreamWithError:(NSError *)error
457 {
458     [self _destroyStreamWithReason:[[self class] reasonForError:error]];
459 }
460
461 - (void)cancelLoadAndDestroyStreamWithError:(NSError *)error
462 {
463     [self retain];
464     [self cancelLoadWithError:error];
465     [self destroyStreamWithError:error];
466     [self setPlugin:NULL];
467     [self release];
468 }
469
470 - (void)_deliverData
471 {
472     if (!stream.ndata || [deliveryData length] == 0)
473         return;
474
475     [self retain];
476
477     int32 totalBytes = [deliveryData length];
478     int32 totalBytesDelivered = 0;
479
480     while (totalBytesDelivered < totalBytes) {
481         WebBaseNetscapePluginView *pv = pluginView;
482         [pv willCallPlugInFunction];
483         int32 deliveryBytes = NPP_WriteReady(plugin, &stream);
484         [pv didCallPlugInFunction];
485         LOG(Plugins, "NPP_WriteReady responseURL=%@ bytes=%d", responseURL, deliveryBytes);
486
487         if (isTerminated)
488             goto exit;
489
490         if (deliveryBytes <= 0) {
491             // Plug-in can't receive anymore data right now. Send it later.
492             [self performSelector:@selector(_deliverData) withObject:nil afterDelay:0];
493             break;
494         } else {
495             deliveryBytes = MIN(deliveryBytes, totalBytes - totalBytesDelivered);
496             NSData *subdata = [deliveryData subdataWithRange:NSMakeRange(totalBytesDelivered, deliveryBytes)];
497             pv = pluginView;
498             [pv willCallPlugInFunction];
499             deliveryBytes = NPP_Write(plugin, &stream, offset, [subdata length], (void *)[subdata bytes]);
500             [pv didCallPlugInFunction];
501             if (deliveryBytes < 0) {
502                 // Netscape documentation says that a negative result from NPP_Write means cancel the load.
503                 [self cancelLoadAndDestroyStreamWithError:[self _pluginCancelledConnectionError]];
504                 return;
505             }
506             deliveryBytes = MIN((unsigned)deliveryBytes, [subdata length]);
507             offset += deliveryBytes;
508             totalBytesDelivered += deliveryBytes;
509             LOG(Plugins, "NPP_Write responseURL=%@ bytes=%d total-delivered=%d/%d", responseURL, deliveryBytes, offset, stream.end);
510         }
511     }
512
513     if (totalBytesDelivered > 0) {
514         if (totalBytesDelivered < totalBytes) {
515             NSMutableData *newDeliveryData = [[NSMutableData alloc] initWithCapacity:totalBytes - totalBytesDelivered];
516             [newDeliveryData appendBytes:(char *)[deliveryData bytes] + totalBytesDelivered length:totalBytes - totalBytesDelivered];
517             [deliveryData release];
518             deliveryData = newDeliveryData;
519         } else {
520             [deliveryData setLength:0];
521             if (reason != WEB_REASON_NONE) {
522                 [self _destroyStream];
523             }
524         }
525     }
526
527 exit:
528     [self release];
529 }
530
531 - (void)_deliverDataToFile:(NSData *)data
532 {
533     if (fileDescriptor == -1 && !path) {
534         NSString *temporaryFileMask = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPlugInStreamXXXXXX"];
535         char *temporaryFileName = strdup([temporaryFileMask fileSystemRepresentation]);
536         fileDescriptor = mkstemp(temporaryFileName);
537         if (fileDescriptor == -1) {
538             LOG_ERROR("Can't create a temporary file.");
539             // This is not a network error, but the only error codes are "network error" and "user break".
540             [self _destroyStreamWithReason:NPRES_NETWORK_ERR];
541             free(temporaryFileName);
542             return;
543         }
544
545         path = [[NSString stringWithUTF8String:temporaryFileName] retain];
546         free(temporaryFileName);
547     }
548
549     int dataLength = [data length];
550     if (!dataLength)
551         return;
552
553     int byteCount = write(fileDescriptor, [data bytes], dataLength);
554     if (byteCount != dataLength) {
555         // This happens only rarely, when we are out of disk space or have a disk I/O error.
556         LOG_ERROR("error writing to temporary file, errno %d", errno);
557         close(fileDescriptor);
558         fileDescriptor = -1;
559
560         // This is not a network error, but the only error codes are "network error" and "user break".
561         [self _destroyStreamWithReason:NPRES_NETWORK_ERR];
562         [path release];
563         path = nil;
564     }
565 }
566
567 - (void)finishedLoading
568 {
569     if (!stream.ndata)
570         return;
571
572     if (transferMode == NP_ASFILE || transferMode == NP_ASFILEONLY) {
573         // Fake the delivery of an empty data to ensure that the file has been created
574         [self _deliverDataToFile:[NSData data]];
575         if (fileDescriptor != -1)
576             close(fileDescriptor);
577         fileDescriptor = -1;
578     }
579
580     [self _destroyStreamWithReason:NPRES_DONE];
581 }
582
583 - (void)receivedData:(NSData *)data
584 {
585     ASSERT([data length] > 0);
586     
587     if (transferMode != NP_ASFILEONLY) {
588         if (!deliveryData) {
589             deliveryData = [[NSMutableData alloc] initWithCapacity:[data length]];
590         }
591         [deliveryData appendData:data];
592         [self _deliverData];
593     }
594     if (transferMode == NP_ASFILE || transferMode == NP_ASFILEONLY)
595         [self _deliverDataToFile:data];
596
597 }
598
599 @end
600
601 static NSString *CarbonPathFromPOSIXPath(NSString *posixPath)
602 {
603     // Doesn't add a trailing colon for directories; this is a problem for paths to a volume,
604     // so this function would need to be revised if we ever wanted to call it with that.
605
606     CFURLRef url = (CFURLRef)[NSURL fileURLWithPath:posixPath];
607     if (!url)
608         return nil;
609
610     return WebCFAutorelease(CFURLCopyFileSystemPath(url, kCFURLHFSPathStyle));
611 }
612
613 #endif