Reviewed by Eric.
[WebKit-https.git] / WebKit / Plugins / WebBaseNetscapePluginStream.m
1 /*
2  * Copyright (C) 2005 Apple Computer, 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 #import <WebKit/WebBaseNetscapePluginStream.h>
30
31 #import <WebKit/WebBaseNetscapePluginView.h>
32 #import <WebKit/WebKitErrorsPrivate.h>
33 #import <WebKit/WebKitLogging.h>
34 #import <WebKit/WebNetscapePluginPackage.h>
35 #import <WebKit/WebNSObjectExtras.h>
36 #import <WebKit/WebNSURLExtras.h>
37 #import <WebKitSystemInterface.h>
38
39 #import <Foundation/NSURLResponse.h>
40
41 static const char *CarbonPathFromPOSIXPath(const char *posixPath);
42
43 #define WEB_REASON_NONE -1
44
45 @implementation WebBaseNetscapePluginStream
46
47 + (NPReason)reasonForError:(NSError *)error
48 {
49     if (error == nil) {
50         return NPRES_DONE;
51     }
52     if ([[error domain] isEqualToString:NSURLErrorDomain] && [error code] == NSURLErrorCancelled) {
53         return NPRES_USER_BREAK;
54     }
55     return NPRES_NETWORK_ERR;
56 }
57
58 - (NSError *)_pluginCancelledConnectionError
59 {
60     return [[[NSError alloc] _initWithPluginErrorCode:WebKitErrorPlugInCancelledConnection
61                                            contentURL:responseURL != nil ? responseURL : requestURL
62                                         pluginPageURL:nil
63                                            pluginName:[[pluginView plugin] name]
64                                              MIMEType:MIMEType] autorelease];
65 }
66
67 - (NSError *)errorForReason:(NPReason)theReason
68 {
69     if (theReason == NPRES_DONE) {
70         return nil;
71     }
72     if (theReason == NPRES_USER_BREAK) {
73         return [NSError _webKitErrorWithDomain:NSURLErrorDomain
74                                           code:NSURLErrorCancelled 
75                                            URL:responseURL != nil ? responseURL : requestURL];
76     }
77     return [self _pluginCancelledConnectionError];
78 }
79
80 - (id)initWithRequestURL:(NSURL *)theRequestURL
81            pluginPointer:(NPP)thePluginPointer
82               notifyData:(void *)theNotifyData
83         sendNotification:(BOOL)flag
84 {
85     [super init];
86  
87     // Temporarily set isTerminated to YES to avoid assertion failure in dealloc in case we are released in this method.
88     isTerminated = YES;
89
90     if (theRequestURL == nil || thePluginPointer == NULL) {
91         [self release];
92         return nil;
93     }
94     
95     [self setRequestURL:theRequestURL];
96     [self setPluginPointer:thePluginPointer];
97     notifyData = theNotifyData;
98     sendNotification = flag;
99     
100     isTerminated = NO;
101     
102     return self;
103 }
104
105 - (void)dealloc
106 {
107     ASSERT(!instance);
108     ASSERT(isTerminated);
109     ASSERT(stream.ndata == nil);
110
111     // The stream file should have been deleted, and the path freed, in -_destroyStream
112     ASSERT(!path);
113
114     [requestURL release];
115     [responseURL release];
116     [MIMEType release];
117     [pluginView release];
118     [deliveryData release];
119     
120     free((void *)stream.url);
121     free(path);
122
123     [super dealloc];
124 }
125
126 - (void)finalize
127 {
128     ASSERT(isTerminated);
129     ASSERT(stream.ndata == nil);
130
131     // The stream file should have been deleted, and the path freed, in -_destroyStream
132     ASSERT(!path);
133
134     free((void *)stream.url);
135     free(path);
136
137     [super finalize];
138 }
139
140 - (uint16)transferMode
141 {
142     return transferMode;
143 }
144
145 - (void)setRequestURL:(NSURL *)theRequestURL
146 {
147     [theRequestURL retain];
148     [requestURL release];
149     requestURL = theRequestURL;
150 }
151
152 - (void)setResponseURL:(NSURL *)theResponseURL
153 {
154     [theResponseURL retain];
155     [responseURL release];
156     responseURL = theResponseURL;
157 }
158
159 - (void)setPluginPointer:(NPP)pluginPointer
160 {
161     if (pluginPointer) {
162         instance = pluginPointer;
163         pluginView = [(WebBaseNetscapePluginView *)instance->ndata retain];
164         WebNetscapePluginPackage *plugin = [pluginView plugin];
165         NPP_NewStream = [plugin NPP_NewStream];
166         NPP_WriteReady = [plugin NPP_WriteReady];
167         NPP_Write = [plugin NPP_Write];
168         NPP_StreamAsFile = [plugin NPP_StreamAsFile];
169         NPP_DestroyStream = [plugin NPP_DestroyStream];
170         NPP_URLNotify = [plugin NPP_URLNotify];
171     } else {
172         instance = NULL;
173         [pluginView release];
174         pluginView = nil;
175         NPP_NewStream = NULL;
176         NPP_WriteReady = NULL;
177         NPP_Write = NULL;
178         NPP_StreamAsFile = NULL;
179         NPP_DestroyStream = NULL;
180         NPP_URLNotify = NULL;
181     }
182 }
183
184 - (void)setMIMEType:(NSString *)theMIMEType
185 {
186     [theMIMEType retain];
187     [MIMEType release];
188     MIMEType = theMIMEType;
189 }
190
191 - (void)startStreamResponseURL:(NSURL *)URL
192          expectedContentLength:(long long)expectedContentLength
193               lastModifiedDate:(NSDate *)lastModifiedDate
194                       MIMEType:(NSString *)theMIMEType
195 {
196     ASSERT(!isTerminated);
197     
198     if (![[pluginView plugin] isLoaded]) {
199         return;
200     }
201     
202     [self setResponseURL:URL];
203     [self setMIMEType:theMIMEType];
204     
205     free((void *)stream.url);
206     stream.url = strdup([responseURL _web_URLCString]);
207
208     stream.ndata = self;
209     stream.end = expectedContentLength > 0 ? expectedContentLength : 0;
210     stream.lastmodified = [lastModifiedDate timeIntervalSince1970];
211     stream.notifyData = notifyData;
212     
213     transferMode = NP_NORMAL;
214     offset = 0;
215     reason = WEB_REASON_NONE;
216
217     // FIXME: Need a way to check if stream is seekable
218
219     NPError npErr = NPP_NewStream(instance, (char *)[MIMEType UTF8String], &stream, NO, &transferMode);
220     LOG(Plugins, "NPP_NewStream URL=%@ MIME=%@ error=%d", responseURL, MIMEType, npErr);
221
222     if (npErr != NPERR_NO_ERROR) {
223         ERROR("NPP_NewStream failed with error: %d responseURL: %@", npErr, responseURL);
224         // Calling cancelLoadWithError: cancels the load, but doesn't call NPP_DestroyStream.
225         [self cancelLoadWithError:[self _pluginCancelledConnectionError]];
226         return;
227     }
228
229     switch (transferMode) {
230         case NP_NORMAL:
231             LOG(Plugins, "Stream type: NP_NORMAL");
232             break;
233         case NP_ASFILEONLY:
234             LOG(Plugins, "Stream type: NP_ASFILEONLY");
235             break;
236         case NP_ASFILE:
237             LOG(Plugins, "Stream type: NP_ASFILE");
238             break;
239         case NP_SEEK:
240             ERROR("Stream type: NP_SEEK not yet supported");
241             [self cancelLoadAndDestroyStreamWithError:[self _pluginCancelledConnectionError]];
242             break;
243         default:
244             ERROR("unknown stream type");
245     }
246 }
247
248 - (void)startStreamWithResponse:(NSURLResponse *)r
249 {
250     [self startStreamResponseURL:[r URL]
251            expectedContentLength:[r expectedContentLength]
252                 lastModifiedDate:WKGetNSURLResponseLastModifiedDate(r)
253                         MIMEType:[r MIMEType]];
254 }
255
256 - (void)_destroyStream
257 {
258     if (isTerminated || ![[pluginView plugin] isLoaded]) {
259         return;
260     }
261     
262     ASSERT(reason != WEB_REASON_NONE);
263     ASSERT([deliveryData length] == 0);
264     
265     if (stream.ndata != NULL) {
266         if (reason == NPRES_DONE && (transferMode == NP_ASFILE || transferMode == NP_ASFILEONLY)) {
267             ASSERT(path != NULL);
268             const char *carbonPath = CarbonPathFromPOSIXPath(path);
269             ASSERT(carbonPath != NULL);
270             NPP_StreamAsFile(instance, &stream, carbonPath);
271
272             // Delete the file after calling NPP_StreamAsFile(), instead of in -dealloc/-finalize.  It should be OK
273             // to delete the file here -- NPP_StreamAsFile() is always called immediately before NPP_DestroyStream()
274             // (the stream destruction function), so there can be no expectation that a plugin will read the stream
275             // file asynchronously after NPP_StreamAsFile() is called.
276             unlink(path);
277             free(path);
278             path = NULL;
279             LOG(Plugins, "NPP_StreamAsFile responseURL=%@ path=%s", responseURL, carbonPath);
280         }
281         
282         NPError npErr;
283         npErr = NPP_DestroyStream(instance, &stream, reason);
284         LOG(Plugins, "NPP_DestroyStream responseURL=%@ error=%d", responseURL, npErr);
285         
286         stream.ndata = nil;
287     }
288     
289     if (sendNotification) {
290         // NPP_URLNotify expects the request URL, not the response URL.
291         NPP_URLNotify(instance, [requestURL _web_URLCString], reason, notifyData);
292         LOG(Plugins, "NPP_URLNotify requestURL=%@ reason=%d", requestURL, reason);
293     }
294     
295     isTerminated = YES;
296
297     [self setPluginPointer:NULL];
298 }
299
300 - (void)_destroyStreamWithReason:(NPReason)theReason
301 {
302     reason = theReason;
303     if (reason != NPRES_DONE) {
304         // Stop any pending data from being streamed.
305         [deliveryData setLength:0];
306     } else if ([deliveryData length] > 0) {
307         // There is more data to be streamed, don't destroy the stream now.
308         return;
309     }
310     [self _destroyStream];
311     ASSERT(stream.ndata == nil);
312 }
313
314 - (void)cancelLoadWithError:(NSError *)error
315 {
316     // Overridden by subclasses.
317     ASSERT_NOT_REACHED();
318 }
319
320 - (void)destroyStreamWithError:(NSError *)error
321 {
322     [self _destroyStreamWithReason:[[self class] reasonForError:error]];
323 }
324
325 - (void)cancelLoadAndDestroyStreamWithError:(NSError *)error
326 {
327     [self cancelLoadWithError:error];
328     [self destroyStreamWithError:error];
329     [self setPluginPointer:NULL];
330 }
331
332 - (void)finishedLoadingWithData:(NSData *)data
333 {
334     if (![[pluginView plugin] isLoaded] || !stream.ndata) {
335         return;
336     }
337     
338     if ((transferMode == NP_ASFILE || transferMode == NP_ASFILEONLY) && !path) {
339         path = strdup("/tmp/WebKitPlugInStreamXXXXXX");
340         int fd = mkstemp(path);
341         if (fd == -1) {
342             // This should almost never happen.
343             ERROR("can't make temporary file, almost certainly a problem with /tmp");
344             // This is not a network error, but the only error codes are "network error" and "user break".
345             [self _destroyStreamWithReason:NPRES_NETWORK_ERR];
346             free(path);
347             path = NULL;
348             return;
349         }
350         int dataLength = [data length];
351         if (dataLength > 0) {
352             int byteCount = write(fd, [data bytes], dataLength);
353             if (byteCount != dataLength) {
354                 // This happens only rarely, when we are out of disk space or have a disk I/O error.
355                 ERROR("error writing to temporary file, errno %d", errno);
356                 close(fd);
357                 // This is not a network error, but the only error codes are "network error" and "user break".
358                 [self _destroyStreamWithReason:NPRES_NETWORK_ERR];
359                 free(path);
360                 path = NULL;
361                 return;
362             }
363         }
364         close(fd);
365     }
366
367     [self _destroyStreamWithReason:NPRES_DONE];
368 }
369
370 - (void)_deliverData
371 {
372     if (![[pluginView plugin] isLoaded] || !stream.ndata || [deliveryData length] == 0) {
373         return;
374     }
375     
376     int32 totalBytes = [deliveryData length];
377     int32 totalBytesDelivered = 0;
378     
379     while (totalBytesDelivered < totalBytes) {
380         int32 deliveryBytes = NPP_WriteReady(instance, &stream);
381         LOG(Plugins, "NPP_WriteReady responseURL=%@ bytes=%d", responseURL, deliveryBytes);
382         
383         if (deliveryBytes <= 0) {
384             // Plug-in can't receive anymore data right now. Send it later.
385             [self performSelector:@selector(_deliverData) withObject:nil afterDelay:0];
386             break;
387         } else {
388             deliveryBytes = MIN(deliveryBytes, totalBytes - totalBytesDelivered);
389             NSData *subdata = [deliveryData subdataWithRange:NSMakeRange(totalBytesDelivered, deliveryBytes)];
390             deliveryBytes = NPP_Write(instance, &stream, offset, [subdata length], (void *)[subdata bytes]);
391             if (deliveryBytes < 0) {
392                 // Netscape documentation says that a negative result from NPP_Write means cancel the load.
393                 [self cancelLoadAndDestroyStreamWithError:[self _pluginCancelledConnectionError]];
394                 return;
395             }
396             deliveryBytes = MIN((unsigned)deliveryBytes, [subdata length]);
397             offset += deliveryBytes;
398             totalBytesDelivered += deliveryBytes;
399             LOG(Plugins, "NPP_Write responseURL=%@ bytes=%d total-delivered=%d/%d", responseURL, deliveryBytes, offset, stream.end);
400         }
401     }
402     
403     if (totalBytesDelivered > 0) {
404         if (totalBytesDelivered < totalBytes) {
405             NSMutableData *newDeliveryData = [[NSMutableData alloc] initWithCapacity:totalBytes - totalBytesDelivered];
406             [newDeliveryData appendBytes:(char *)[deliveryData bytes] + totalBytesDelivered length:totalBytes - totalBytesDelivered];
407             [deliveryData release];
408             deliveryData = newDeliveryData;
409         } else {
410             [deliveryData setLength:0];
411             if (reason != WEB_REASON_NONE) {
412                 [self _destroyStream];
413             }
414         }
415     }
416 }
417
418 - (void)receivedData:(NSData *)data
419 {
420     ASSERT([data length] > 0);
421     
422     if (transferMode != NP_ASFILEONLY) {
423         if (!deliveryData) {
424             deliveryData = [[NSMutableData alloc] initWithCapacity:[data length]];
425         }
426         [deliveryData appendData:data];
427         [self _deliverData];
428     }
429 }
430
431 @end
432
433 static const char *CarbonPathFromPOSIXPath(const char *posixPath)
434 {
435     // Returns NULL if path is to file that does not exist.
436     // Doesn't add a trailing colon for directories; this is a problem for paths to a volume,
437     // so this function would need to be revised if we ever wanted to call it with that.
438
439     OSStatus error;
440     FSCatalogInfo info;
441
442     // Make an FSRef.
443     FSRef ref;
444     error = FSPathMakeRef((const UInt8 *)posixPath, &ref, NULL);
445     if (error != noErr) {
446         return NULL;
447     }
448
449     // Get volume refNum.
450     error = FSGetCatalogInfo(&ref, kFSCatInfoVolume, &info, NULL, NULL, NULL);
451     if (error != noErr) {
452         return NULL;
453     }
454
455     // Get root directory FSRef.
456     FSRef rootRef;
457     error = FSGetVolumeInfo(info.volume, 0, NULL, kFSVolInfoNone, NULL, NULL, &rootRef);
458     if (error != noErr) {
459         return NULL;
460     }
461
462     // Get the pieces of the path.
463     NSMutableData *carbonPath = [NSMutableData dataWithBytes:"" length:1];
464     BOOL needColon = NO;
465     for (;;) {
466         FSSpec spec;
467         FSRef parentRef;
468         error = FSGetCatalogInfo(&ref, kFSCatInfoNone, NULL, NULL, &spec, &parentRef);
469         if (error != noErr) {
470             return NULL;
471         }
472         if (needColon) {
473             [carbonPath replaceBytesInRange:NSMakeRange(0, 0) withBytes:":" length:1];
474         }
475         [carbonPath replaceBytesInRange:NSMakeRange(0, 0) withBytes:&spec.name[1] length:spec.name[0]];
476         needColon = YES;
477         if (FSCompareFSRefs(&ref, &rootRef) == noErr) {
478             break;
479         }
480         ref = parentRef;
481     }
482
483     return (const char *)[carbonPath bytes];
484 }