2 * Copyright (C) 2005, 2006, 2007 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
29 #if ENABLE(NETSCAPE_PLUGIN_API)
30 #import "WebBaseNetscapePluginStream.h"
32 #import "WebBaseNetscapePluginView.h"
33 #import "WebFrameInternal.h"
34 #import "WebKitErrorsPrivate.h"
35 #import "WebKitLogging.h"
36 #import "WebNSObjectExtras.h"
37 #import "WebNSURLExtras.h"
38 #import "WebNSURLRequestExtras.h"
39 #import "WebNetscapePluginPackage.h"
40 #import "WebNetscapePlugInStreamLoaderClient.h"
41 #import <Foundation/NSURLResponse.h>
42 #import <kjs/JSLock.h>
43 #import <WebCore/DocumentLoader.h>
44 #import <WebCore/Frame.h>
45 #import <WebCore/FrameLoader.h>
46 #import <WebCore/WebCoreObjCExtras.h>
47 #import <WebKitSystemInterface.h>
48 #import <wtf/HashMap.h>
50 using namespace WebCore;
52 #define WEB_REASON_NONE -1
54 static NSString *CarbonPathFromPOSIXPath(NSString *posixPath);
56 typedef HashMap<NPStream*, NPP> StreamMap;
57 static StreamMap& streams()
59 static StreamMap staticStreams;
63 @implementation WebBaseNetscapePluginStream
65 #ifndef BUILDING_ON_TIGER
68 WebCoreObjCFinalizeOnMainThread(self);
72 + (NPP)ownerForStream:(NPStream *)stream
74 return streams().get(stream);
77 + (NPReason)reasonForError:(NSError *)error
82 if ([[error domain] isEqualToString:NSURLErrorDomain] && [error code] == NSURLErrorCancelled) {
83 return NPRES_USER_BREAK;
85 return NPRES_NETWORK_ERR;
88 - (NSError *)_pluginCancelledConnectionError
90 return [[[NSError alloc] _initWithPluginErrorCode:WebKitErrorPlugInCancelledConnection
91 contentURL:responseURL != nil ? responseURL : requestURL
93 pluginName:[[pluginView pluginPackage] name]
94 MIMEType:MIMEType] autorelease];
97 - (NSError *)errorForReason:(NPReason)theReason
99 if (theReason == NPRES_DONE) {
102 if (theReason == NPRES_USER_BREAK) {
103 return [NSError _webKitErrorWithDomain:NSURLErrorDomain
104 code:NSURLErrorCancelled
105 URL:responseURL != nil ? responseURL : requestURL];
107 return [self _pluginCancelledConnectionError];
110 - (id)initWithFrameLoader:(FrameLoader *)frameLoader
112 _frameLoader = frameLoader;
117 - (id)initWithRequest:(NSURLRequest *)theRequest
118 plugin:(NPP)thePlugin
119 notifyData:(void *)theNotifyData
120 sendNotification:(BOOL)flag
122 WebBaseNetscapePluginView *view = (WebBaseNetscapePluginView *)thePlugin->ndata;
124 if (!core([view webFrame])->loader()->canLoad([theRequest URL], String(), core([view webFrame])->document()))
127 if ([self initWithRequestURL:[theRequest URL]
129 notifyData:theNotifyData
130 sendNotification:flag] == nil) {
134 // Temporarily set isTerminated to YES to avoid assertion failure in dealloc in case we are released in this method.
137 request = [theRequest mutableCopy];
138 if (core([view webFrame])->loader()->shouldHideReferrer([theRequest URL], core([view webFrame])->loader()->outgoingReferrer()))
139 [(NSMutableURLRequest *)request _web_setHTTPReferrer:nil];
141 _client = new WebNetscapePlugInStreamLoaderClient(self);
142 _loader = NetscapePlugInStreamLoader::create(core([view webFrame]), _client).releaseRef();
143 _loader->setShouldBufferData(false);
150 - (id)initWithRequestURL:(NSURL *)theRequestURL
151 plugin:(NPP)thePlugin
152 notifyData:(void *)theNotifyData
153 sendNotification:(BOOL)flag
157 // Temporarily set isTerminated to YES to avoid assertion failure in dealloc in case we are released in this method.
160 if (theRequestURL == nil || thePlugin == NULL) {
165 [self setRequestURL:theRequestURL];
166 [self setPlugin:thePlugin];
167 notifyData = theNotifyData;
168 sendNotification = flag;
170 newStreamSuccessful = NO;
172 streams().add(&stream, thePlugin);
182 ASSERT(isTerminated);
183 ASSERT(stream.ndata == nil);
185 // The stream file should have been deleted, and the path freed, in -_destroyStream
187 ASSERT(fileDescriptor == -1);
194 [requestURL release];
195 [responseURL release];
197 [pluginView release];
198 [deliveryData release];
200 free((void *)stream.url);
204 streams().remove(&stream);
211 ASSERT_MAIN_THREAD();
212 ASSERT(isTerminated);
213 ASSERT(stream.ndata == nil);
215 // The stream file should have been deleted, and the path freed, in -_destroyStream
217 ASSERT(fileDescriptor == -1);
223 free((void *)stream.url);
227 streams().remove(&stream);
232 - (uint16)transferMode
242 - (void)setRequestURL:(NSURL *)theRequestURL
244 [theRequestURL retain];
245 [requestURL release];
246 requestURL = theRequestURL;
249 - (void)setResponseURL:(NSURL *)theResponseURL
251 [theResponseURL retain];
252 [responseURL release];
253 responseURL = theResponseURL;
256 - (void)setPlugin:(NPP)thePlugin
260 pluginView = [(WebBaseNetscapePluginView *)plugin->ndata retain];
261 WebNetscapePluginPackage *pluginPackage = [pluginView pluginPackage];
263 pluginFuncs = [pluginPackage pluginFuncs];
265 WebBaseNetscapePluginView *view = pluginView;
272 [view disconnectStream:self];
277 - (void)setMIMEType:(NSString *)theMIMEType
279 [theMIMEType retain];
281 MIMEType = theMIMEType;
284 - (void)startStreamResponseURL:(NSURL *)URL
285 expectedContentLength:(long long)expectedContentLength
286 lastModifiedDate:(NSDate *)lastModifiedDate
287 MIMEType:(NSString *)theMIMEType
288 headers:(NSData *)theHeaders
290 ASSERT(!isTerminated);
292 [self setResponseURL:URL];
293 [self setMIMEType:theMIMEType];
295 free((void *)stream.url);
296 stream.url = strdup([responseURL _web_URLCString]);
299 stream.end = expectedContentLength > 0 ? (uint32)expectedContentLength : 0;
300 stream.lastmodified = (uint32)[lastModifiedDate timeIntervalSince1970];
301 stream.notifyData = notifyData;
304 unsigned len = [theHeaders length];
305 headers = (char*) malloc(len + 1);
306 [theHeaders getBytes:headers];
308 stream.headers = headers;
311 transferMode = NP_NORMAL;
313 reason = WEB_REASON_NONE;
314 // FIXME: If WebNetscapePluginStream called our initializer we wouldn't have to do this here.
317 // FIXME: Need a way to check if stream is seekable
319 WebBaseNetscapePluginView *pv = pluginView;
320 [pv willCallPlugInFunction];
321 NPError npErr = pluginFuncs->newstream(plugin, (char *)[MIMEType UTF8String], &stream, NO, &transferMode);
322 [pv didCallPlugInFunction];
323 LOG(Plugins, "NPP_NewStream URL=%@ MIME=%@ error=%d", responseURL, MIMEType, npErr);
325 if (npErr != NPERR_NO_ERROR) {
326 LOG_ERROR("NPP_NewStream failed with error: %d responseURL: %@", npErr, responseURL);
327 // Calling cancelLoadWithError: cancels the load, but doesn't call NPP_DestroyStream.
328 [self cancelLoadWithError:[self _pluginCancelledConnectionError]];
332 newStreamSuccessful = YES;
334 switch (transferMode) {
336 LOG(Plugins, "Stream type: NP_NORMAL");
339 LOG(Plugins, "Stream type: NP_ASFILEONLY");
342 LOG(Plugins, "Stream type: NP_ASFILE");
345 LOG_ERROR("Stream type: NP_SEEK not yet supported");
346 [self cancelLoadAndDestroyStreamWithError:[self _pluginCancelledConnectionError]];
349 LOG_ERROR("unknown stream type");
356 ASSERT(!_frameLoader);
358 _loader->documentLoader()->addPlugInStreamLoader(_loader);
359 _loader->load(request);
364 ASSERT(!_frameLoader);
366 if (!_loader->isDone())
367 [self cancelLoadAndDestroyStreamWithError:_loader->cancelledError()];
370 - (void)startStreamWithResponse:(NSURLResponse *)r
372 NSMutableData *theHeaders = nil;
373 long long expectedContentLength = [r expectedContentLength];
375 if ([r isKindOfClass:[NSHTTPURLResponse class]]) {
376 NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)r;
377 theHeaders = [NSMutableData dataWithCapacity:1024];
379 // FIXME: it would be nice to be able to get the raw HTTP header block.
380 // This includes the HTTP version, the real status text,
381 // all headers in their original order and including duplicates,
382 // and all original bytes verbatim, rather than sent through Unicode translation.
383 // Unfortunately NSHTTPURLResponse doesn't provide access at that low a level.
385 [theHeaders appendBytes:"HTTP " length:5];
387 long statusCode = [httpResponse statusCode];
388 snprintf(statusStr, sizeof(statusStr), "%ld", statusCode);
389 [theHeaders appendBytes:statusStr length:strlen(statusStr)];
390 [theHeaders appendBytes:" OK\n" length:4];
392 // HACK: pass the headers through as UTF-8.
393 // This is not the intended behavior; we're supposed to pass original bytes verbatim.
394 // But we don't have the original bytes, we have NSStrings built by the URL loading system.
395 // It hopefully shouldn't matter, since RFC2616/RFC822 require ASCII-only headers,
396 // but surely someone out there is using non-ASCII characters, and hopefully UTF-8 is adequate here.
397 // It seems better than NSASCIIStringEncoding, which will lose information if non-ASCII is used.
399 NSDictionary *headerDict = [httpResponse allHeaderFields];
400 NSArray *keys = [[headerDict allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
401 NSEnumerator *i = [keys objectEnumerator];
403 while ((k = [i nextObject]) != nil) {
404 NSString *v = [headerDict objectForKey:k];
405 [theHeaders appendData:[k dataUsingEncoding:NSUTF8StringEncoding]];
406 [theHeaders appendBytes:": " length:2];
407 [theHeaders appendData:[v dataUsingEncoding:NSUTF8StringEncoding]];
408 [theHeaders appendBytes:"\n" length:1];
411 // If the content is encoded (most likely compressed), then don't send its length to the plugin,
412 // which is only interested in the decoded length, not yet known at the moment.
413 // <rdar://problem/4470599> tracks a request for -[NSURLResponse expectedContentLength] to incorporate this logic.
414 NSString *contentEncoding = (NSString *)[[(NSHTTPURLResponse *)r allHeaderFields] objectForKey:@"Content-Encoding"];
415 if (contentEncoding && ![contentEncoding isEqualToString:@"identity"])
416 expectedContentLength = -1;
418 // startStreamResponseURL:... will null-terminate.
421 [self startStreamResponseURL:[r URL]
422 expectedContentLength:expectedContentLength
423 lastModifiedDate:WKGetNSURLResponseLastModifiedDate(r)
424 MIMEType:[r MIMEType]
428 - (BOOL)wantsAllStreams
430 if (!pluginFuncs->getvalue)
435 WebBaseNetscapePluginView *pv = pluginView;
436 [pv willCallPlugInFunction];
438 JSC::JSLock::DropAllLocks dropAllLocks(false);
439 error = pluginFuncs->getvalue(plugin, NPPVpluginWantsAllNetworkStreams, &value);
441 [pv didCallPlugInFunction];
442 if (error != NPERR_NO_ERROR)
448 - (void)_destroyStream
455 ASSERT(reason != WEB_REASON_NONE);
456 ASSERT([deliveryData length] == 0);
458 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_deliverData) object:nil];
460 if (stream.ndata != nil) {
461 if (reason == NPRES_DONE && (transferMode == NP_ASFILE || transferMode == NP_ASFILEONLY)) {
462 ASSERT(fileDescriptor == -1);
463 ASSERT(path != NULL);
464 NSString *carbonPath = CarbonPathFromPOSIXPath(path);
465 ASSERT(carbonPath != NULL);
466 WebBaseNetscapePluginView *pv = pluginView;
467 [pv willCallPlugInFunction];
468 pluginFuncs->asfile(plugin, &stream, [carbonPath fileSystemRepresentation]);
469 [pv didCallPlugInFunction];
470 LOG(Plugins, "NPP_StreamAsFile responseURL=%@ path=%s", responseURL, carbonPath);
474 // Delete the file after calling NPP_StreamAsFile(), instead of in -dealloc/-finalize. It should be OK
475 // to delete the file here -- NPP_StreamAsFile() is always called immediately before NPP_DestroyStream()
476 // (the stream destruction function), so there can be no expectation that a plugin will read the stream
477 // file asynchronously after NPP_StreamAsFile() is called.
478 unlink([path fileSystemRepresentation]);
486 if (fileDescriptor != -1) {
487 // The file may still be open if we are destroying the stream before it completed loading.
488 close(fileDescriptor);
492 if (newStreamSuccessful) {
494 WebBaseNetscapePluginView *pv = pluginView;
495 [pv willCallPlugInFunction];
496 npErr = pluginFuncs->destroystream(plugin, &stream, reason);
497 [pv didCallPlugInFunction];
498 LOG(Plugins, "NPP_DestroyStream responseURL=%@ error=%d", responseURL, npErr);
503 stream.headers = NULL;
511 if (sendNotification) {
512 // NPP_URLNotify expects the request URL, not the response URL.
513 WebBaseNetscapePluginView *pv = pluginView;
514 [pv willCallPlugInFunction];
515 pluginFuncs->urlnotify(plugin, [requestURL _web_URLCString], reason, notifyData);
516 [pv didCallPlugInFunction];
517 LOG(Plugins, "NPP_URLNotify requestURL=%@ reason=%d", requestURL, reason);
522 [self setPlugin:NULL];
528 - (void)_destroyStreamWithReason:(NPReason)theReason
531 if (reason != NPRES_DONE) {
532 // Stop any pending data from being streamed.
533 [deliveryData setLength:0];
534 } else if ([deliveryData length] > 0) {
535 // There is more data to be streamed, don't destroy the stream now.
538 [self _destroyStream];
539 ASSERT(stream.ndata == nil);
542 - (void)cancelLoadWithError:(NSError *)error
547 DocumentLoader* documentLoader = _frameLoader->activeDocumentLoader();
548 ASSERT(documentLoader);
550 if (documentLoader->isLoadingMainResource())
551 documentLoader->cancelMainResourceLoad(error);
555 if (!_loader->isDone())
556 _loader->cancel(error);
559 - (void)destroyStreamWithError:(NSError *)error
561 [self _destroyStreamWithReason:[[self class] reasonForError:error]];
564 - (void)cancelLoadAndDestroyStreamWithError:(NSError *)error
567 [self cancelLoadWithError:error];
568 [self destroyStreamWithError:error];
569 [self setPlugin:NULL];
575 if (!stream.ndata || [deliveryData length] == 0)
580 int32 totalBytes = [deliveryData length];
581 int32 totalBytesDelivered = 0;
583 while (totalBytesDelivered < totalBytes) {
584 WebBaseNetscapePluginView *pv = pluginView;
585 [pv willCallPlugInFunction];
586 int32 deliveryBytes = pluginFuncs->writeready(plugin, &stream);
587 [pv didCallPlugInFunction];
588 LOG(Plugins, "NPP_WriteReady responseURL=%@ bytes=%d", responseURL, deliveryBytes);
593 if (deliveryBytes <= 0) {
594 // Plug-in can't receive anymore data right now. Send it later.
595 [self performSelector:@selector(_deliverData) withObject:nil afterDelay:0];
598 deliveryBytes = MIN(deliveryBytes, totalBytes - totalBytesDelivered);
599 NSData *subdata = [deliveryData subdataWithRange:NSMakeRange(totalBytesDelivered, deliveryBytes)];
601 [pv willCallPlugInFunction];
602 deliveryBytes = pluginFuncs->write(plugin, &stream, offset, [subdata length], (void *)[subdata bytes]);
603 [pv didCallPlugInFunction];
604 if (deliveryBytes < 0) {
605 // Netscape documentation says that a negative result from NPP_Write means cancel the load.
606 [self cancelLoadAndDestroyStreamWithError:[self _pluginCancelledConnectionError]];
609 deliveryBytes = MIN((unsigned)deliveryBytes, [subdata length]);
610 offset += deliveryBytes;
611 totalBytesDelivered += deliveryBytes;
612 LOG(Plugins, "NPP_Write responseURL=%@ bytes=%d total-delivered=%d/%d", responseURL, deliveryBytes, offset, stream.end);
616 if (totalBytesDelivered > 0) {
617 if (totalBytesDelivered < totalBytes) {
618 NSMutableData *newDeliveryData = [[NSMutableData alloc] initWithCapacity:totalBytes - totalBytesDelivered];
619 [newDeliveryData appendBytes:(char *)[deliveryData bytes] + totalBytesDelivered length:totalBytes - totalBytesDelivered];
620 [deliveryData release];
621 deliveryData = newDeliveryData;
623 [deliveryData setLength:0];
624 if (reason != WEB_REASON_NONE) {
625 [self _destroyStream];
634 - (void)_deliverDataToFile:(NSData *)data
636 if (fileDescriptor == -1 && !path) {
637 NSString *temporaryFileMask = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPlugInStreamXXXXXX"];
638 char *temporaryFileName = strdup([temporaryFileMask fileSystemRepresentation]);
639 fileDescriptor = mkstemp(temporaryFileName);
640 if (fileDescriptor == -1) {
641 LOG_ERROR("Can't create a temporary file.");
642 // This is not a network error, but the only error codes are "network error" and "user break".
643 [self _destroyStreamWithReason:NPRES_NETWORK_ERR];
644 free(temporaryFileName);
648 path = [[NSString stringWithUTF8String:temporaryFileName] retain];
649 free(temporaryFileName);
652 int dataLength = [data length];
656 int byteCount = write(fileDescriptor, [data bytes], dataLength);
657 if (byteCount != dataLength) {
658 // This happens only rarely, when we are out of disk space or have a disk I/O error.
659 LOG_ERROR("error writing to temporary file, errno %d", errno);
660 close(fileDescriptor);
663 // This is not a network error, but the only error codes are "network error" and "user break".
664 [self _destroyStreamWithReason:NPRES_NETWORK_ERR];
670 - (void)finishedLoading
675 if (transferMode == NP_ASFILE || transferMode == NP_ASFILEONLY) {
676 // Fake the delivery of an empty data to ensure that the file has been created
677 [self _deliverDataToFile:[NSData data]];
678 if (fileDescriptor != -1)
679 close(fileDescriptor);
683 [self _destroyStreamWithReason:NPRES_DONE];
686 - (void)receivedData:(NSData *)data
688 ASSERT([data length] > 0);
690 if (transferMode != NP_ASFILEONLY) {
692 deliveryData = [[NSMutableData alloc] initWithCapacity:[data length]];
694 [deliveryData appendData:data];
697 if (transferMode == NP_ASFILE || transferMode == NP_ASFILEONLY)
698 [self _deliverDataToFile:data];
704 static NSString *CarbonPathFromPOSIXPath(NSString *posixPath)
706 // Doesn't add a trailing colon for directories; this is a problem for paths to a volume,
707 // so this function would need to be revised if we ever wanted to call it with that.
709 CFURLRef url = (CFURLRef)[NSURL fileURLWithPath:posixPath];
713 return WebCFAutorelease(CFURLCopyFileSystemPath(url, kCFURLHFSPathStyle));