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 WebNetscapePluginStream::ownerForStream(NPStream *stream)
74 return streams().get(stream);
77 NPReason WebNetscapePluginStream::reasonForError(NSError *error)
82 if ([[error domain] isEqualToString:NSURLErrorDomain] && [error code] == NSURLErrorCancelled)
83 return NPRES_USER_BREAK;
85 return NPRES_NETWORK_ERR;
88 NSError *WebNetscapePluginStream::pluginCancelledConnectionError() const
90 return [[[NSError alloc] _initWithPluginErrorCode:WebKitErrorPlugInCancelledConnection
91 contentURL:m_responseURL ? m_responseURL.get() : m_requestURL.get()
93 pluginName:[[m_pluginView.get() pluginPackage] name]
94 MIMEType:m_mimeType.get()] autorelease];
97 NSError *WebNetscapePluginStream::errorForReason(NPReason reason) const
99 if (reason == NPRES_DONE)
102 if (reason == NPRES_USER_BREAK)
103 return [NSError _webKitErrorWithDomain:NSURLErrorDomain
104 code:NSURLErrorCancelled
105 URL:m_responseURL ? m_responseURL.get() : m_requestURL.get()];
107 return pluginCancelledConnectionError();
110 - (NSError *)errorForReason:(NPReason)theReason
112 return _impl->errorForReason(theReason);
115 - (id)initWithFrameLoader:(FrameLoader *)frameLoader
119 _impl = WebNetscapePluginStream::create(self);
120 _impl->m_frameLoader = frameLoader;
125 - (id)initWithRequest:(NSURLRequest *)theRequest
126 plugin:(NPP)thePlugin
127 notifyData:(void *)theNotifyData
128 sendNotification:(BOOL)flag
130 WebBaseNetscapePluginView *view = (WebBaseNetscapePluginView *)thePlugin->ndata;
132 // This check has already been done by the plug-in view.
133 ASSERT(FrameLoader::canLoad([theRequest URL], String(), core([view webFrame])->document()));
135 ASSERT([theRequest URL]);
138 _impl = WebNetscapePluginStream::create(self);
140 // Temporarily set isTerminated to true to avoid assertion failure in dealloc in case we are released in this method.
141 _impl->m_isTerminated = true;
143 _impl->m_requestURL = [theRequest URL];
144 _impl->setPlugin(thePlugin);
145 _impl->m_notifyData = theNotifyData;
146 _impl->m_sendNotification = flag;
147 _impl->m_fileDescriptor = -1;
148 _impl->m_newStreamSuccessful = false;
150 streams().add(&_impl->m_stream, thePlugin);
152 _impl->m_request = [theRequest mutableCopy];
153 if (core([view webFrame])->loader()->shouldHideReferrer([theRequest URL], core([view webFrame])->loader()->outgoingReferrer()))
154 [(NSMutableURLRequest *)_impl->m_request _web_setHTTPReferrer:nil];
156 _impl->m_client = new WebNetscapePlugInStreamLoaderClient(self);
157 _impl->m_loader = NetscapePlugInStreamLoader::create(core([view webFrame]), _impl->m_client).releaseRef();
158 _impl->m_loader->setShouldBufferData(false);
160 _impl->m_isTerminated = false;
167 ASSERT(!_impl->m_plugin);
168 ASSERT(_impl->m_isTerminated);
169 ASSERT(_impl->m_stream.ndata == nil);
171 // The stream file should have been deleted, and the path freed, in -_destroyStream
172 ASSERT(!_impl->m_path);
173 ASSERT(_impl->m_fileDescriptor == -1);
176 _impl->m_loader->deref();
177 delete _impl->m_client;
178 [_impl->m_request release];
180 free((void *)_impl->m_stream.url);
181 free(_impl->m_headers);
183 streams().remove(&_impl->m_stream);
192 ASSERT_MAIN_THREAD();
193 ASSERT(_impl->m_isTerminated);
194 ASSERT(_impl->m_stream.ndata == nil);
196 // The stream file should have been deleted, and the path freed, in -_destroyStream
197 ASSERT(!_impl->m_path);
198 ASSERT(_impl->m_fileDescriptor == -1);
201 _impl->m_loader->deref();
202 delete _impl->m_client;
204 free((void *)_impl->m_stream.url);
205 free(_impl->m_headers);
207 streams().remove(&_impl->m_stream);
216 return _impl->m_plugin;
219 - (void)setRequestURL:(NSURL *)theRequestURL
221 _impl->m_requestURL = theRequestURL;
224 - (void)setPlugin:(NPP)thePlugin
226 _impl->setPlugin(thePlugin);
229 void WebNetscapePluginStream::setPlugin(NPP plugin)
233 m_pluginView = static_cast<WebBaseNetscapePluginView *>(m_plugin->ndata);
235 WebNetscapePluginPackage *pluginPackage = [m_pluginView.get() pluginPackage];
237 m_pluginFuncs = [pluginPackage pluginFuncs];
239 WebBaseNetscapePluginView *view = m_pluginView.get();
243 [view disconnectStream:m_pluginStream];
248 void WebNetscapePluginStream::startStream(NSURL *url, long long expectedContentLength, NSDate *lastModifiedDate, NSString *mimeType, NSData *headers)
250 ASSERT(!m_isTerminated);
253 m_mimeType = mimeType;
255 free((void *)m_stream.url);
256 m_stream.url = strdup([m_responseURL.get() _web_URLCString]);
258 m_stream.ndata = m_pluginStream;
259 m_stream.end = expectedContentLength > 0 ? (uint32)expectedContentLength : 0;
260 m_stream.lastmodified = (uint32)[lastModifiedDate timeIntervalSince1970];
261 m_stream.notifyData = m_notifyData;
264 unsigned len = [headers length];
265 m_headers = (char*) malloc(len + 1);
266 [headers getBytes:m_headers];
268 m_stream.headers = m_headers;
271 m_transferMode = NP_NORMAL;
273 m_reason = WEB_REASON_NONE;
274 // FIXME: If WebNetscapePluginStream called our initializer we wouldn't have to do this here.
275 m_fileDescriptor = -1;
277 // FIXME: Need a way to check if stream is seekable
279 [m_pluginView.get() willCallPlugInFunction];
280 NPError npErr = m_pluginFuncs->newstream(m_plugin, (char *)[m_mimeType.get() UTF8String], &m_stream, NO, &m_transferMode);
281 [m_pluginView.get() didCallPlugInFunction];
282 LOG(Plugins, "NPP_NewStream URL=%@ MIME=%@ error=%d", m_responseURL.get(), m_mimeType.get(), npErr);
284 if (npErr != NPERR_NO_ERROR) {
285 LOG_ERROR("NPP_NewStream failed with error: %d responseURL: %@", npErr, m_responseURL.get());
286 // Calling cancelLoadWithError: cancels the load, but doesn't call NPP_DestroyStream.
287 cancelLoadWithError(pluginCancelledConnectionError());
291 m_newStreamSuccessful = true;
293 switch (m_transferMode) {
295 LOG(Plugins, "Stream type: NP_NORMAL");
298 LOG(Plugins, "Stream type: NP_ASFILEONLY");
301 LOG(Plugins, "Stream type: NP_ASFILE");
304 LOG_ERROR("Stream type: NP_SEEK not yet supported");
305 cancelLoadAndDestroyStreamWithError(pluginCancelledConnectionError());
308 LOG_ERROR("unknown stream type");
314 ASSERT(_impl->m_request);
315 ASSERT(!_impl->m_frameLoader);
317 _impl->m_loader->documentLoader()->addPlugInStreamLoader(_impl->m_loader);
318 _impl->m_loader->load(_impl->m_request);
321 - (void)startStreamWithResponse:(NSURLResponse *)r
323 NSMutableData *theHeaders = nil;
324 long long expectedContentLength = [r expectedContentLength];
326 if ([r isKindOfClass:[NSHTTPURLResponse class]]) {
327 NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)r;
328 theHeaders = [NSMutableData dataWithCapacity:1024];
330 // FIXME: it would be nice to be able to get the raw HTTP header block.
331 // This includes the HTTP version, the real status text,
332 // all headers in their original order and including duplicates,
333 // and all original bytes verbatim, rather than sent through Unicode translation.
334 // Unfortunately NSHTTPURLResponse doesn't provide access at that low a level.
336 [theHeaders appendBytes:"HTTP " length:5];
338 long statusCode = [httpResponse statusCode];
339 snprintf(statusStr, sizeof(statusStr), "%ld", statusCode);
340 [theHeaders appendBytes:statusStr length:strlen(statusStr)];
341 [theHeaders appendBytes:" OK\n" length:4];
343 // HACK: pass the headers through as UTF-8.
344 // This is not the intended behavior; we're supposed to pass original bytes verbatim.
345 // But we don't have the original bytes, we have NSStrings built by the URL loading system.
346 // It hopefully shouldn't matter, since RFC2616/RFC822 require ASCII-only headers,
347 // but surely someone out there is using non-ASCII characters, and hopefully UTF-8 is adequate here.
348 // It seems better than NSASCIIStringEncoding, which will lose information if non-ASCII is used.
350 NSDictionary *headerDict = [httpResponse allHeaderFields];
351 NSArray *keys = [[headerDict allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
352 NSEnumerator *i = [keys objectEnumerator];
354 while ((k = [i nextObject]) != nil) {
355 NSString *v = [headerDict objectForKey:k];
356 [theHeaders appendData:[k dataUsingEncoding:NSUTF8StringEncoding]];
357 [theHeaders appendBytes:": " length:2];
358 [theHeaders appendData:[v dataUsingEncoding:NSUTF8StringEncoding]];
359 [theHeaders appendBytes:"\n" length:1];
362 // If the content is encoded (most likely compressed), then don't send its length to the plugin,
363 // which is only interested in the decoded length, not yet known at the moment.
364 // <rdar://problem/4470599> tracks a request for -[NSURLResponse expectedContentLength] to incorporate this logic.
365 NSString *contentEncoding = (NSString *)[[(NSHTTPURLResponse *)r allHeaderFields] objectForKey:@"Content-Encoding"];
366 if (contentEncoding && ![contentEncoding isEqualToString:@"identity"])
367 expectedContentLength = -1;
369 // startStreamResponseURL:... will null-terminate.
372 _impl->startStream([r URL], expectedContentLength, WKGetNSURLResponseLastModifiedDate(r), [r MIMEType], theHeaders);
375 - (BOOL)wantsAllStreams
377 if (!_impl->m_pluginFuncs->getvalue)
382 WebBaseNetscapePluginView *pv = _impl->m_pluginView.get();
383 [pv willCallPlugInFunction];
385 JSC::JSLock::DropAllLocks dropAllLocks(false);
386 error = _impl->m_pluginFuncs->getvalue(_impl->m_plugin, NPPVpluginWantsAllNetworkStreams, &value);
388 [pv didCallPlugInFunction];
389 if (error != NPERR_NO_ERROR)
395 void WebNetscapePluginStream::destroyStream()
400 RetainPtr<WebBaseNetscapePluginStream> protect(m_pluginStream);
402 ASSERT(m_reason != WEB_REASON_NONE);
403 ASSERT([m_deliveryData.get() length] == 0);
405 m_deliverDataTimer.stop();
407 if (m_stream.ndata != nil) {
408 if (m_reason == NPRES_DONE && (m_transferMode == NP_ASFILE || m_transferMode == NP_ASFILEONLY)) {
409 ASSERT(m_fileDescriptor == -1);
411 NSString *carbonPath = CarbonPathFromPOSIXPath(m_path.get());
412 ASSERT(carbonPath != NULL);
413 [m_pluginView.get() willCallPlugInFunction];
414 m_pluginFuncs->asfile(m_plugin, &m_stream, [carbonPath fileSystemRepresentation]);
415 [m_pluginView.get() didCallPlugInFunction];
416 LOG(Plugins, "NPP_StreamAsFile responseURL=%@ path=%s", m_responseURL.get(), carbonPath);
420 // Delete the file after calling NPP_StreamAsFile(), instead of in -dealloc/-finalize. It should be OK
421 // to delete the file here -- NPP_StreamAsFile() is always called immediately before NPP_DestroyStream()
422 // (the stream destruction function), so there can be no expectation that a plugin will read the stream
423 // file asynchronously after NPP_StreamAsFile() is called.
424 unlink([m_path.get() fileSystemRepresentation]);
431 if (m_fileDescriptor != -1) {
432 // The file may still be open if we are destroying the stream before it completed loading.
433 close(m_fileDescriptor);
434 m_fileDescriptor = -1;
437 if (m_newStreamSuccessful) {
438 [m_pluginView.get() willCallPlugInFunction];
442 m_pluginFuncs->destroystream(m_plugin, &m_stream, m_reason);
443 [m_pluginView.get() didCallPlugInFunction];
444 LOG(Plugins, "NPP_DestroyStream responseURL=%@ error=%d", m_responseURL.get(), npErr);
449 m_stream.headers = NULL;
451 m_stream.ndata = nil;
457 if (m_sendNotification) {
458 // NPP_URLNotify expects the request URL, not the response URL.
459 [m_pluginView.get() willCallPlugInFunction];
460 m_pluginFuncs->urlnotify(m_plugin, [m_requestURL.get() _web_URLCString], m_reason, m_notifyData);
461 [m_pluginView.get() didCallPlugInFunction];
462 LOG(Plugins, "NPP_URLNotify requestURL=%@ reason=%d", m_requestURL.get(), m_reason);
465 m_isTerminated = true;
470 void WebNetscapePluginStream::destroyStreamWithReason(NPReason reason)
473 if (m_reason != NPRES_DONE) {
474 // Stop any pending data from being streamed.
475 [m_deliveryData.get() setLength:0];
476 } else if ([m_deliveryData.get() length] > 0) {
477 // There is more data to be streamed, don't destroy the stream now.
481 ASSERT(m_stream.ndata == nil);
484 void WebNetscapePluginStream::cancelLoadWithError(NSError *error)
489 DocumentLoader* documentLoader = m_frameLoader->activeDocumentLoader();
490 ASSERT(documentLoader);
492 if (documentLoader->isLoadingMainResource())
493 documentLoader->cancelMainResourceLoad(error);
497 if (!m_loader->isDone())
498 m_loader->cancel(error);
501 - (void)cancelLoadWithError:(NSError *)error
503 _impl->cancelLoadWithError(error);
506 void WebNetscapePluginStream::destroyStreamWithError(NSError *error)
508 destroyStreamWithReason(reasonForError(error));
511 - (void)destroyStreamWithError:(NSError *)error
513 _impl->destroyStreamWithError(error);
516 void WebNetscapePluginStream::cancelLoadAndDestroyStreamWithError(NSError *error)
518 RetainPtr<WebBaseNetscapePluginStream> protect(m_pluginStream);
519 cancelLoadWithError(error);
520 destroyStreamWithError(error);
524 - (void)cancelLoadAndDestroyStreamWithError:(NSError *)error
526 return _impl->cancelLoadAndDestroyStreamWithError(error);
529 void WebNetscapePluginStream::deliverData()
531 if (!m_stream.ndata || [m_deliveryData.get() length] == 0)
534 RetainPtr<WebBaseNetscapePluginStream> protect(m_pluginStream);
536 int32 totalBytes = [m_deliveryData.get() length];
537 int32 totalBytesDelivered = 0;
539 while (totalBytesDelivered < totalBytes) {
540 [m_pluginView.get() willCallPlugInFunction];
541 int32 deliveryBytes = m_pluginFuncs->writeready(m_plugin, &m_stream);
542 [m_pluginView.get() didCallPlugInFunction];
543 LOG(Plugins, "NPP_WriteReady responseURL=%@ bytes=%d", m_responseURL.get(), deliveryBytes);
548 if (deliveryBytes <= 0) {
549 // Plug-in can't receive anymore data right now. Send it later.
550 if (!m_deliverDataTimer.isActive())
551 m_deliverDataTimer.startOneShot(0);
554 deliveryBytes = MIN(deliveryBytes, totalBytes - totalBytesDelivered);
555 NSData *subdata = [m_deliveryData.get() subdataWithRange:NSMakeRange(totalBytesDelivered, deliveryBytes)];
556 [m_pluginView.get() willCallPlugInFunction];
557 deliveryBytes = m_pluginFuncs->write(m_plugin, &m_stream, m_offset, [subdata length], (void *)[subdata bytes]);
558 [m_pluginView.get() didCallPlugInFunction];
559 if (deliveryBytes < 0) {
560 // Netscape documentation says that a negative result from NPP_Write means cancel the load.
561 cancelLoadAndDestroyStreamWithError(pluginCancelledConnectionError());
564 deliveryBytes = MIN((unsigned)deliveryBytes, [subdata length]);
565 m_offset += deliveryBytes;
566 totalBytesDelivered += deliveryBytes;
567 LOG(Plugins, "NPP_Write responseURL=%@ bytes=%d total-delivered=%d/%d", m_responseURL.get(), deliveryBytes, m_offset, m_stream.end);
571 if (totalBytesDelivered > 0) {
572 if (totalBytesDelivered < totalBytes) {
573 NSMutableData *newDeliveryData = [[NSMutableData alloc] initWithCapacity:totalBytes - totalBytesDelivered];
574 [newDeliveryData appendBytes:(char *)[m_deliveryData.get() bytes] + totalBytesDelivered length:totalBytes - totalBytesDelivered];
575 [m_deliveryData.get() release];
576 m_deliveryData = newDeliveryData;
577 [newDeliveryData release];
579 [m_deliveryData.get() setLength:0];
580 if (m_reason != WEB_REASON_NONE)
586 void WebNetscapePluginStream::deliverDataTimerFired(WebCore::Timer<WebNetscapePluginStream>* timer)
591 void WebNetscapePluginStream::deliverDataToFile(NSData *data)
593 if (m_fileDescriptor == -1 && !m_path) {
594 NSString *temporaryFileMask = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPlugInStreamXXXXXX"];
595 char *temporaryFileName = strdup([temporaryFileMask fileSystemRepresentation]);
596 m_fileDescriptor = mkstemp(temporaryFileName);
597 if (m_fileDescriptor == -1) {
598 LOG_ERROR("Can't create a temporary file.");
599 // This is not a network error, but the only error codes are "network error" and "user break".
600 destroyStreamWithReason(NPRES_NETWORK_ERR);
601 free(temporaryFileName);
605 m_path.adoptNS([[NSString stringWithUTF8String:temporaryFileName] retain]);
606 free(temporaryFileName);
609 int dataLength = [data length];
613 int byteCount = write(m_fileDescriptor, [data bytes], dataLength);
614 if (byteCount != dataLength) {
615 // This happens only rarely, when we are out of disk space or have a disk I/O error.
616 LOG_ERROR("error writing to temporary file, errno %d", errno);
617 close(m_fileDescriptor);
618 m_fileDescriptor = -1;
620 // This is not a network error, but the only error codes are "network error" and "user break".
621 destroyStreamWithReason(NPRES_NETWORK_ERR);
626 - (void)finishedLoading
628 if (!_impl->m_stream.ndata)
631 if (_impl->m_transferMode == NP_ASFILE || _impl->m_transferMode == NP_ASFILEONLY) {
632 // Fake the delivery of an empty data to ensure that the file has been created
633 _impl->deliverDataToFile([NSData data]);
634 if (_impl->m_fileDescriptor != -1)
635 close(_impl->m_fileDescriptor);
636 _impl->m_fileDescriptor = -1;
639 _impl->destroyStreamWithReason(NPRES_DONE);
642 - (void)receivedData:(NSData *)data
644 ASSERT([data length] > 0);
646 if (_impl->m_transferMode != NP_ASFILEONLY) {
647 if (!_impl->m_deliveryData)
648 _impl->m_deliveryData.adoptNS([[NSMutableData alloc] initWithCapacity:[data length]]);
649 [_impl->m_deliveryData.get() appendData:data];
650 _impl->deliverData();
652 if (_impl->m_transferMode == NP_ASFILE || _impl->m_transferMode == NP_ASFILEONLY)
653 _impl->deliverDataToFile(data);
658 static NSString *CarbonPathFromPOSIXPath(NSString *posixPath)
660 // Doesn't add a trailing colon for directories; this is a problem for paths to a volume,
661 // so this function would need to be revised if we ever wanted to call it with that.
663 CFURLRef url = (CFURLRef)[NSURL fileURLWithPath:posixPath];
667 return WebCFAutorelease(CFURLCopyFileSystemPath(url, kCFURLHFSPathStyle));