9e7ad416ce1d0cea90f94c08be7f0c04f11a61ca
[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     // FIXME: It's generally considered bad style to do work, like deleting a file,
112     // at dealloc time. We should change things around so that this is done at a
113     // more well-defined time rather than when the last release happens.
114     if (path) {
115         unlink(path);
116     }
117
118     [requestURL release];
119     [responseURL release];
120     [MIMEType release];
121     [pluginView release];
122     [deliveryData release];
123     
124     free((void *)stream.url);
125     free(path);
126
127     [super dealloc];
128 }
129
130 - (void)finalize
131 {
132     ASSERT(isTerminated);
133     ASSERT(stream.ndata == nil);
134
135     // FIXME: Bad for all the reasons mentioned above, but even worse for GC.
136     if (path) {
137         unlink(path);
138     }
139
140     free((void *)stream.url);
141     free(path);
142
143     [super finalize];
144 }
145
146 - (uint16)transferMode
147 {
148     return transferMode;
149 }
150
151 - (void)setRequestURL:(NSURL *)theRequestURL
152 {
153     [theRequestURL retain];
154     [requestURL release];
155     requestURL = theRequestURL;
156 }
157
158 - (void)setResponseURL:(NSURL *)theResponseURL
159 {
160     [theResponseURL retain];
161     [responseURL release];
162     responseURL = theResponseURL;
163 }
164
165 - (void)setPluginPointer:(NPP)pluginPointer
166 {
167     if (pluginPointer) {
168         instance = pluginPointer;
169         pluginView = [(WebBaseNetscapePluginView *)instance->ndata retain];
170         WebNetscapePluginPackage *plugin = [pluginView plugin];
171         NPP_NewStream = [plugin NPP_NewStream];
172         NPP_WriteReady = [plugin NPP_WriteReady];
173         NPP_Write = [plugin NPP_Write];
174         NPP_StreamAsFile = [plugin NPP_StreamAsFile];
175         NPP_DestroyStream = [plugin NPP_DestroyStream];
176         NPP_URLNotify = [plugin NPP_URLNotify];
177     } else {
178         instance = NULL;
179         [pluginView release];
180         pluginView = nil;
181         NPP_NewStream = NULL;
182         NPP_WriteReady = NULL;
183         NPP_Write = NULL;
184         NPP_StreamAsFile = NULL;
185         NPP_DestroyStream = NULL;
186         NPP_URLNotify = NULL;
187     }
188 }
189
190 - (void)setMIMEType:(NSString *)theMIMEType
191 {
192     [theMIMEType retain];
193     [MIMEType release];
194     MIMEType = theMIMEType;
195 }
196
197 - (void)startStreamResponseURL:(NSURL *)URL
198          expectedContentLength:(long long)expectedContentLength
199               lastModifiedDate:(NSDate *)lastModifiedDate
200                       MIMEType:(NSString *)theMIMEType
201 {
202     ASSERT(!isTerminated);
203     
204     if (![[pluginView plugin] isLoaded]) {
205         return;
206     }
207     
208     [self setResponseURL:URL];
209     [self setMIMEType:theMIMEType];
210     
211     free((void *)stream.url);
212     stream.url = strdup([responseURL _web_URLCString]);
213
214     stream.ndata = self;
215     stream.end = expectedContentLength > 0 ? expectedContentLength : 0;
216     stream.lastmodified = [lastModifiedDate timeIntervalSince1970];
217     stream.notifyData = notifyData;
218     
219     transferMode = NP_NORMAL;
220     offset = 0;
221     reason = WEB_REASON_NONE;
222
223     // FIXME: Need a way to check if stream is seekable
224
225     NPError npErr = NPP_NewStream(instance, (char *)[MIMEType UTF8String], &stream, NO, &transferMode);
226     LOG(Plugins, "NPP_NewStream URL=%@ MIME=%@ error=%d", responseURL, MIMEType, npErr);
227
228     if (npErr != NPERR_NO_ERROR) {
229         ERROR("NPP_NewStream failed with error: %d responseURL: %@", npErr, responseURL);
230         // Calling cancelLoadWithError: cancels the load, but doesn't call NPP_DestroyStream.
231         [self cancelLoadWithError:[self _pluginCancelledConnectionError]];
232         return;
233     }
234
235     switch (transferMode) {
236         case NP_NORMAL:
237             LOG(Plugins, "Stream type: NP_NORMAL");
238             break;
239         case NP_ASFILEONLY:
240             LOG(Plugins, "Stream type: NP_ASFILEONLY");
241             break;
242         case NP_ASFILE:
243             LOG(Plugins, "Stream type: NP_ASFILE");
244             break;
245         case NP_SEEK:
246             ERROR("Stream type: NP_SEEK not yet supported");
247             [self cancelLoadAndDestroyStreamWithError:[self _pluginCancelledConnectionError]];
248             break;
249         default:
250             ERROR("unknown stream type");
251     }
252 }
253
254 - (void)startStreamWithResponse:(NSURLResponse *)r
255 {
256     [self startStreamResponseURL:[r URL]
257            expectedContentLength:[r expectedContentLength]
258                 lastModifiedDate:WKGetNSURLResponseLastModifiedDate(r)
259                         MIMEType:[r MIMEType]];
260 }
261
262 - (void)_destroyStream
263 {
264     if (isTerminated || ![[pluginView plugin] isLoaded]) {
265         return;
266     }
267     
268     ASSERT(reason != WEB_REASON_NONE);
269     ASSERT([deliveryData length] == 0);
270     
271     if (stream.ndata != NULL) {
272         if (reason == NPRES_DONE && (transferMode == NP_ASFILE || transferMode == NP_ASFILEONLY)) {
273             ASSERT(path != NULL);
274             const char *carbonPath = CarbonPathFromPOSIXPath(path);
275             ASSERT(carbonPath != NULL);
276             NPP_StreamAsFile(instance, &stream, carbonPath);
277             LOG(Plugins, "NPP_StreamAsFile responseURL=%@ path=%s", responseURL, carbonPath);
278         }
279         
280         NPError npErr;
281         npErr = NPP_DestroyStream(instance, &stream, reason);
282         LOG(Plugins, "NPP_DestroyStream responseURL=%@ error=%d", responseURL, npErr);
283         
284         stream.ndata = nil;
285     }
286     
287     if (sendNotification) {
288         // NPP_URLNotify expects the request URL, not the response URL.
289         NPP_URLNotify(instance, [requestURL _web_URLCString], reason, notifyData);
290         LOG(Plugins, "NPP_URLNotify requestURL=%@ reason=%d", requestURL, reason);
291     }
292     
293     isTerminated = YES;
294
295     [self setPluginPointer:NULL];
296 }
297
298 - (void)_destroyStreamWithReason:(NPReason)theReason
299 {
300     reason = theReason;
301     if (reason != NPRES_DONE) {
302         // Stop any pending data from being streamed.
303         [deliveryData setLength:0];
304     } else if ([deliveryData length] > 0) {
305         // There is more data to be streamed, don't destroy the stream now.
306         return;
307     }
308     [self _destroyStream];
309     ASSERT(stream.ndata == nil);
310 }
311
312 - (void)cancelLoadWithError:(NSError *)error
313 {
314     // Overridden by subclasses.
315     ASSERT_NOT_REACHED();
316 }
317
318 - (void)destroyStreamWithError:(NSError *)error
319 {
320     [self _destroyStreamWithReason:[[self class] reasonForError:error]];
321 }
322
323 - (void)cancelLoadAndDestroyStreamWithError:(NSError *)error
324 {
325     [self cancelLoadWithError:error];
326     [self destroyStreamWithError:error];
327     [self setPluginPointer:NULL];
328 }
329
330 - (void)finishedLoadingWithData:(NSData *)data
331 {
332     if (![[pluginView plugin] isLoaded] || !stream.ndata) {
333         return;
334     }
335     
336     if ((transferMode == NP_ASFILE || transferMode == NP_ASFILEONLY) && !path) {
337         path = strdup("/tmp/WebKitPlugInStreamXXXXXX");
338         int fd = mkstemp(path);
339         if (fd == -1) {
340             // This should almost never happen.
341             ERROR("can't make temporary file, almost certainly a problem with /tmp");
342             // This is not a network error, but the only error codes are "network error" and "user break".
343             [self _destroyStreamWithReason:NPRES_NETWORK_ERR];
344             free(path);
345             path = NULL;
346             return;
347         }
348         int dataLength = [data length];
349         if (dataLength > 0) {
350             int byteCount = write(fd, [data bytes], dataLength);
351             if (byteCount != dataLength) {
352                 // This happens only rarely, when we are out of disk space or have a disk I/O error.
353                 ERROR("error writing to temporary file, errno %d", errno);
354                 close(fd);
355                 // This is not a network error, but the only error codes are "network error" and "user break".
356                 [self _destroyStreamWithReason:NPRES_NETWORK_ERR];
357                 free(path);
358                 path = NULL;
359                 return;
360             }
361         }
362         close(fd);
363     }
364
365     [self _destroyStreamWithReason:NPRES_DONE];
366 }
367
368 - (void)_deliverData
369 {
370     if (![[pluginView plugin] isLoaded] || !stream.ndata || [deliveryData length] == 0) {
371         return;
372     }
373     
374     int32 totalBytes = [deliveryData length];
375     int32 totalBytesDelivered = 0;
376     
377     while (totalBytesDelivered < totalBytes) {
378         int32 deliveryBytes = NPP_WriteReady(instance, &stream);
379         LOG(Plugins, "NPP_WriteReady responseURL=%@ bytes=%d", responseURL, deliveryBytes);
380         
381         if (deliveryBytes <= 0) {
382             // Plug-in can't receive anymore data right now. Send it later.
383             [self performSelector:@selector(_deliverData) withObject:nil afterDelay:0];
384             break;
385         } else {
386             deliveryBytes = MIN(deliveryBytes, totalBytes - totalBytesDelivered);
387             NSData *subdata = [deliveryData subdataWithRange:NSMakeRange(totalBytesDelivered, deliveryBytes)];
388             deliveryBytes = NPP_Write(instance, &stream, offset, [subdata length], (void *)[subdata bytes]);
389             if (deliveryBytes < 0) {
390                 // Netscape documentation says that a negative result from NPP_Write means cancel the load.
391                 [self cancelLoadAndDestroyStreamWithError:[self _pluginCancelledConnectionError]];
392                 return;
393             }
394             deliveryBytes = MIN((unsigned)deliveryBytes, [subdata length]);
395             offset += deliveryBytes;
396             totalBytesDelivered += deliveryBytes;
397             LOG(Plugins, "NPP_Write responseURL=%@ bytes=%d total-delivered=%d/%d", responseURL, deliveryBytes, offset, stream.end);
398         }
399     }
400     
401     if (totalBytesDelivered > 0) {
402         if (totalBytesDelivered < totalBytes) {
403             NSMutableData *newDeliveryData = [[NSMutableData alloc] initWithCapacity:totalBytes - totalBytesDelivered];
404             [newDeliveryData appendBytes:(char *)[deliveryData bytes] + totalBytesDelivered length:totalBytes - totalBytesDelivered];
405             [deliveryData release];
406             deliveryData = newDeliveryData;
407         } else {
408             [deliveryData setLength:0];
409             if (reason != WEB_REASON_NONE) {
410                 [self _destroyStream];
411             }
412         }
413     }
414 }
415
416 - (void)receivedData:(NSData *)data
417 {
418     ASSERT([data length] > 0);
419     
420     if (transferMode != NP_ASFILEONLY) {
421         if (!deliveryData) {
422             deliveryData = [[NSMutableData alloc] initWithCapacity:[data length]];
423         }
424         [deliveryData appendData:data];
425         [self _deliverData];
426     }
427 }
428
429 @end
430
431 static const char *CarbonPathFromPOSIXPath(const char *posixPath)
432 {
433     // Returns NULL if path is to file that does not exist.
434     // Doesn't add a trailing colon for directories; this is a problem for paths to a volume,
435     // so this function would need to be revised if we ever wanted to call it with that.
436
437     OSStatus error;
438     FSCatalogInfo info;
439
440     // Make an FSRef.
441     FSRef ref;
442     error = FSPathMakeRef((const UInt8 *)posixPath, &ref, NULL);
443     if (error != noErr) {
444         return NULL;
445     }
446
447     // Get volume refNum.
448     error = FSGetCatalogInfo(&ref, kFSCatInfoVolume, &info, NULL, NULL, NULL);
449     if (error != noErr) {
450         return NULL;
451     }
452
453     // Get root directory FSRef.
454     FSRef rootRef;
455     error = FSGetVolumeInfo(info.volume, 0, NULL, kFSVolInfoNone, NULL, NULL, &rootRef);
456     if (error != noErr) {
457         return NULL;
458     }
459
460     // Get the pieces of the path.
461     NSMutableData *carbonPath = [NSMutableData dataWithBytes:"" length:1];
462     BOOL needColon = NO;
463     for (;;) {
464         FSSpec spec;
465         FSRef parentRef;
466         error = FSGetCatalogInfo(&ref, kFSCatInfoNone, NULL, NULL, &spec, &parentRef);
467         if (error != noErr) {
468             return NULL;
469         }
470         if (needColon) {
471             [carbonPath replaceBytesInRange:NSMakeRange(0, 0) withBytes:":" length:1];
472         }
473         [carbonPath replaceBytesInRange:NSMakeRange(0, 0) withBytes:&spec.name[1] length:spec.name[0]];
474         needColon = YES;
475         if (FSCompareFSRefs(&ref, &rootRef) == noErr) {
476             break;
477         }
478         ref = parentRef;
479     }
480
481     return (const char *)[carbonPath bytes];
482 }