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