diff -r 000000000000 -r 4f2f89ce4247 WebKit/mac/Plugins/WebNetscapePluginStream.mm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebKit/mac/Plugins/WebNetscapePluginStream.mm Fri Sep 17 09:02:29 2010 +0300 @@ -0,0 +1,643 @@ +/* + * Copyright (C) 2005, 2006, 2007 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#if ENABLE(NETSCAPE_PLUGIN_API) +#import "WebNetscapePluginStream.h" + +#import "WebNetscapePluginView.h" +#import "WebFrameInternal.h" +#import "WebKitErrorsPrivate.h" +#import "WebKitLogging.h" +#import "WebNSObjectExtras.h" +#import "WebNSURLExtras.h" +#import "WebNSURLRequestExtras.h" +#import "WebNetscapePluginPackage.h" +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +using namespace WebCore; +using namespace std; + +#define WEB_REASON_NONE -1 + +static NSString *CarbonPathFromPOSIXPath(NSString *posixPath); + +class PluginStopDeferrer { +public: + PluginStopDeferrer(WebNetscapePluginView* pluginView) + : m_pluginView(pluginView) + { + ASSERT(m_pluginView); + + [m_pluginView.get() willCallPlugInFunction]; + } + + ~PluginStopDeferrer() + { + ASSERT(m_pluginView); + [m_pluginView.get() didCallPlugInFunction]; + } + +private: + RetainPtr m_pluginView; +}; + +typedef HashMap StreamMap; +static StreamMap& streams() +{ + DEFINE_STATIC_LOCAL(StreamMap, staticStreams, ()); + return staticStreams; +} + +NPP WebNetscapePluginStream::ownerForStream(NPStream *stream) +{ + return streams().get(stream); +} + +NPReason WebNetscapePluginStream::reasonForError(NSError *error) +{ + if (!error) + return NPRES_DONE; + + if ([[error domain] isEqualToString:NSURLErrorDomain] && [error code] == NSURLErrorCancelled) + return NPRES_USER_BREAK; + + return NPRES_NETWORK_ERR; +} + +NSError *WebNetscapePluginStream::pluginCancelledConnectionError() const +{ + return [[[NSError alloc] _initWithPluginErrorCode:WebKitErrorPlugInCancelledConnection + contentURL:m_responseURL ? m_responseURL.get() : (NSURL *)m_requestURL + pluginPageURL:nil + pluginName:[[m_pluginView.get() pluginPackage] pluginInfo].name + MIMEType:(NSString *)String::fromUTF8(m_mimeType.data(), m_mimeType.length())] autorelease]; +} + +NSError *WebNetscapePluginStream::errorForReason(NPReason reason) const +{ + if (reason == NPRES_DONE) + return nil; + + if (reason == NPRES_USER_BREAK) + return [NSError _webKitErrorWithDomain:NSURLErrorDomain + code:NSURLErrorCancelled + URL:m_responseURL ? m_responseURL.get() : (NSURL *)m_requestURL]; + + return pluginCancelledConnectionError(); +} + +WebNetscapePluginStream::WebNetscapePluginStream(FrameLoader* frameLoader) + : m_plugin(0) + , m_transferMode(0) + , m_offset(0) + , m_fileDescriptor(-1) + , m_sendNotification(false) + , m_notifyData(0) + , m_headers(0) + , m_reason(NPRES_BASE) + , m_isTerminated(false) + , m_newStreamSuccessful(false) + , m_frameLoader(frameLoader) + , m_pluginFuncs(0) + , m_deliverDataTimer(this, &WebNetscapePluginStream::deliverDataTimerFired) +{ + memset(&m_stream, 0, sizeof(NPStream)); +} + +WebNetscapePluginStream::WebNetscapePluginStream(NSURLRequest *request, NPP plugin, bool sendNotification, void* notifyData) + : m_requestURL([request URL]) + , m_plugin(0) + , m_transferMode(0) + , m_offset(0) + , m_fileDescriptor(-1) + , m_sendNotification(sendNotification) + , m_notifyData(notifyData) + , m_headers(0) + , m_reason(NPRES_BASE) + , m_isTerminated(false) + , m_newStreamSuccessful(false) + , m_frameLoader(0) + , m_request(AdoptNS, [request mutableCopy]) + , m_pluginFuncs(0) + , m_deliverDataTimer(this, &WebNetscapePluginStream::deliverDataTimerFired) +{ + memset(&m_stream, 0, sizeof(NPStream)); + + WebNetscapePluginView *view = (WebNetscapePluginView *)plugin->ndata; + + // This check has already been done by the plug-in view. + ASSERT(SecurityOrigin::canLoad([request URL], String(), core([view webFrame])->document())); + + ASSERT([request URL]); + ASSERT(plugin); + + setPlugin(plugin); + + streams().add(&m_stream, plugin); + + if (SecurityOrigin::shouldHideReferrer([request URL], core([view webFrame])->loader()->outgoingReferrer())) + [m_request.get() _web_setHTTPReferrer:nil]; +} + +WebNetscapePluginStream::~WebNetscapePluginStream() +{ + ASSERT(!m_plugin); + ASSERT(m_isTerminated); + ASSERT(!m_stream.ndata); + + // The stream file should have been deleted, and the path freed, in -_destroyStream + ASSERT(!m_path); + ASSERT(m_fileDescriptor == -1); + + free((void *)m_stream.url); + free(m_headers); + + streams().remove(&m_stream); +} + +void WebNetscapePluginStream::setPlugin(NPP plugin) +{ + if (plugin) { + m_plugin = plugin; + m_pluginView = static_cast(m_plugin->ndata); + + WebNetscapePluginPackage *pluginPackage = [m_pluginView.get() pluginPackage]; + + m_pluginFuncs = [pluginPackage pluginFuncs]; + } else { + WebNetscapePluginView *view = m_pluginView.get(); + m_plugin = 0; + m_pluginFuncs = 0; + + [view disconnectStream:this]; + m_pluginView = 0; + } +} + +void WebNetscapePluginStream::startStream(NSURL *url, long long expectedContentLength, NSDate *lastModifiedDate, const String& mimeType, NSData *headers) +{ + ASSERT(!m_isTerminated); + + m_responseURL = url; + m_mimeType = mimeType.utf8(); + + free((void *)m_stream.url); + m_stream.url = strdup([m_responseURL.get() _web_URLCString]); + + m_stream.ndata = this; + m_stream.end = expectedContentLength > 0 ? (uint32_t)expectedContentLength : 0; + m_stream.lastmodified = (uint32_t)[lastModifiedDate timeIntervalSince1970]; + m_stream.notifyData = m_notifyData; + + if (headers) { + unsigned len = [headers length]; + m_headers = (char*) malloc(len + 1); + [headers getBytes:m_headers]; + m_headers[len] = 0; + m_stream.headers = m_headers; + } + + m_transferMode = NP_NORMAL; + m_offset = 0; + m_reason = WEB_REASON_NONE; + // FIXME: If WebNetscapePluginStream called our initializer we wouldn't have to do this here. + m_fileDescriptor = -1; + + // FIXME: Need a way to check if stream is seekable + + NPError npErr; + { + PluginStopDeferrer deferrer(m_pluginView.get()); + npErr = m_pluginFuncs->newstream(m_plugin, m_mimeType.mutableData(), &m_stream, NO, &m_transferMode); + } + + LOG(Plugins, "NPP_NewStream URL=%@ MIME=%s error=%d", m_responseURL.get(), m_mimeType.data(), npErr); + + if (npErr != NPERR_NO_ERROR) { + LOG_ERROR("NPP_NewStream failed with error: %d responseURL: %@", npErr, m_responseURL.get()); + // Calling cancelLoadWithError: cancels the load, but doesn't call NPP_DestroyStream. + cancelLoadWithError(pluginCancelledConnectionError()); + return; + } + + m_newStreamSuccessful = true; + + switch (m_transferMode) { + case NP_NORMAL: + LOG(Plugins, "Stream type: NP_NORMAL"); + break; + case NP_ASFILEONLY: + LOG(Plugins, "Stream type: NP_ASFILEONLY"); + break; + case NP_ASFILE: + LOG(Plugins, "Stream type: NP_ASFILE"); + break; + case NP_SEEK: + LOG_ERROR("Stream type: NP_SEEK not yet supported"); + cancelLoadAndDestroyStreamWithError(pluginCancelledConnectionError()); + break; + default: + LOG_ERROR("unknown stream type"); + } +} + +void WebNetscapePluginStream::start() +{ + ASSERT(m_request); + ASSERT(!m_frameLoader); + ASSERT(!m_loader); + + m_loader = NetscapePlugInStreamLoader::create(core([m_pluginView.get() webFrame]), this); + m_loader->setShouldBufferData(false); + + m_loader->documentLoader()->addPlugInStreamLoader(m_loader.get()); + m_loader->load(m_request.get()); +} + +void WebNetscapePluginStream::stop() +{ + ASSERT(!m_frameLoader); + + if (!m_loader->isDone()) + cancelLoadAndDestroyStreamWithError(m_loader->cancelledError()); +} + +void WebNetscapePluginStream::didReceiveResponse(NetscapePlugInStreamLoader*, const ResourceResponse& response) +{ + NSURLResponse *r = response.nsURLResponse(); + + NSMutableData *theHeaders = nil; + long long expectedContentLength = [r expectedContentLength]; + + if ([r isKindOfClass:[NSHTTPURLResponse class]]) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)r; + theHeaders = [NSMutableData dataWithCapacity:1024]; + + // FIXME: it would be nice to be able to get the raw HTTP header block. + // This includes the HTTP version, the real status text, + // all headers in their original order and including duplicates, + // and all original bytes verbatim, rather than sent through Unicode translation. + // Unfortunately NSHTTPURLResponse doesn't provide access at that low a level. + + [theHeaders appendBytes:"HTTP " length:5]; + char statusStr[10]; + long statusCode = [httpResponse statusCode]; + snprintf(statusStr, sizeof(statusStr), "%ld", statusCode); + [theHeaders appendBytes:statusStr length:strlen(statusStr)]; + [theHeaders appendBytes:" OK\n" length:4]; + + // HACK: pass the headers through as UTF-8. + // This is not the intended behavior; we're supposed to pass original bytes verbatim. + // But we don't have the original bytes, we have NSStrings built by the URL loading system. + // It hopefully shouldn't matter, since RFC2616/RFC822 require ASCII-only headers, + // but surely someone out there is using non-ASCII characters, and hopefully UTF-8 is adequate here. + // It seems better than NSASCIIStringEncoding, which will lose information if non-ASCII is used. + + NSDictionary *headerDict = [httpResponse allHeaderFields]; + NSArray *keys = [[headerDict allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; + NSEnumerator *i = [keys objectEnumerator]; + NSString *k; + while ((k = [i nextObject]) != nil) { + NSString *v = [headerDict objectForKey:k]; + [theHeaders appendData:[k dataUsingEncoding:NSUTF8StringEncoding]]; + [theHeaders appendBytes:": " length:2]; + [theHeaders appendData:[v dataUsingEncoding:NSUTF8StringEncoding]]; + [theHeaders appendBytes:"\n" length:1]; + } + + // If the content is encoded (most likely compressed), then don't send its length to the plugin, + // which is only interested in the decoded length, not yet known at the moment. + // tracks a request for -[NSURLResponse expectedContentLength] to incorporate this logic. + NSString *contentEncoding = (NSString *)[[(NSHTTPURLResponse *)r allHeaderFields] objectForKey:@"Content-Encoding"]; + if (contentEncoding && ![contentEncoding isEqualToString:@"identity"]) + expectedContentLength = -1; + + // startStreamResponseURL:... will null-terminate. + } + + startStream([r URL], expectedContentLength, WKGetNSURLResponseLastModifiedDate(r), response.mimeType(), theHeaders); +} + +void WebNetscapePluginStream::startStreamWithResponse(NSURLResponse *response) +{ + didReceiveResponse(0, response); +} + +bool WebNetscapePluginStream::wantsAllStreams() const +{ + if (!m_pluginFuncs->getvalue) + return false; + + void *value = 0; + NPError error; + { + PluginStopDeferrer deferrer(m_pluginView.get()); + JSC::JSLock::DropAllLocks dropAllLocks(JSC::SilenceAssertionsOnly); + error = m_pluginFuncs->getvalue(m_plugin, NPPVpluginWantsAllNetworkStreams, &value); + } + if (error != NPERR_NO_ERROR) + return false; + + return value; +} + +void WebNetscapePluginStream::destroyStream() +{ + if (m_isTerminated) + return; + + RefPtr protect(this); + + ASSERT(m_reason != WEB_REASON_NONE); + ASSERT([m_deliveryData.get() length] == 0); + + m_deliverDataTimer.stop(); + + if (m_stream.ndata) { + if (m_reason == NPRES_DONE && (m_transferMode == NP_ASFILE || m_transferMode == NP_ASFILEONLY)) { + ASSERT(m_fileDescriptor == -1); + ASSERT(m_path); + NSString *carbonPath = CarbonPathFromPOSIXPath(m_path.get()); + ASSERT(carbonPath != NULL); + + PluginStopDeferrer deferrer(m_pluginView.get()); + m_pluginFuncs->asfile(m_plugin, &m_stream, [carbonPath fileSystemRepresentation]); + LOG(Plugins, "NPP_StreamAsFile responseURL=%@ path=%s", m_responseURL.get(), carbonPath); + } + + if (m_path) { + // Delete the file after calling NPP_StreamAsFile(), instead of in -dealloc/-finalize. It should be OK + // to delete the file here -- NPP_StreamAsFile() is always called immediately before NPP_DestroyStream() + // (the stream destruction function), so there can be no expectation that a plugin will read the stream + // file asynchronously after NPP_StreamAsFile() is called. + unlink([m_path.get() fileSystemRepresentation]); + m_path = 0; + + if (m_isTerminated) + return; + } + + if (m_fileDescriptor != -1) { + // The file may still be open if we are destroying the stream before it completed loading. + close(m_fileDescriptor); + m_fileDescriptor = -1; + } + + if (m_newStreamSuccessful) { + PluginStopDeferrer deferrer(m_pluginView.get()); +#if !LOG_DISABLED + NPError npErr = +#endif + m_pluginFuncs->destroystream(m_plugin, &m_stream, m_reason); + LOG(Plugins, "NPP_DestroyStream responseURL=%@ error=%d", m_responseURL.get(), npErr); + } + + free(m_headers); + m_headers = NULL; + m_stream.headers = NULL; + + m_stream.ndata = 0; + + if (m_isTerminated) + return; + } + + if (m_sendNotification) { + // NPP_URLNotify expects the request URL, not the response URL. + PluginStopDeferrer deferrer(m_pluginView.get()); + m_pluginFuncs->urlnotify(m_plugin, m_requestURL.string().utf8().data(), m_reason, m_notifyData); + LOG(Plugins, "NPP_URLNotify requestURL=%@ reason=%d", (NSURL *)m_requestURL, m_reason); + } + + m_isTerminated = true; + + setPlugin(0); +} + +void WebNetscapePluginStream::destroyStreamWithReason(NPReason reason) +{ + m_reason = reason; + if (m_reason != NPRES_DONE) { + // Stop any pending data from being streamed. + [m_deliveryData.get() setLength:0]; + } else if ([m_deliveryData.get() length] > 0) { + // There is more data to be streamed, don't destroy the stream now. + return; + } + + RefPtr protect(this); + destroyStream(); + ASSERT(!m_stream.ndata); +} + +void WebNetscapePluginStream::cancelLoadWithError(NSError *error) +{ + if (m_frameLoader) { + ASSERT(!m_loader); + + DocumentLoader* documentLoader = m_frameLoader->activeDocumentLoader(); + ASSERT(documentLoader); + + if (documentLoader->isLoadingMainResource()) + documentLoader->cancelMainResourceLoad(error); + return; + } + + if (!m_loader->isDone()) + m_loader->cancel(error); +} + +void WebNetscapePluginStream::destroyStreamWithError(NSError *error) +{ + destroyStreamWithReason(reasonForError(error)); +} + +void WebNetscapePluginStream::didFail(WebCore::NetscapePlugInStreamLoader*, const WebCore::ResourceError& error) +{ + destroyStreamWithError(error); +} + +void WebNetscapePluginStream::cancelLoadAndDestroyStreamWithError(NSError *error) +{ + RefPtr protect(this); + cancelLoadWithError(error); + destroyStreamWithError(error); + setPlugin(0); +} + +void WebNetscapePluginStream::deliverData() +{ + if (!m_stream.ndata || [m_deliveryData.get() length] == 0) + return; + + RefPtr protect(this); + + int32_t totalBytes = [m_deliveryData.get() length]; + int32_t totalBytesDelivered = 0; + + while (totalBytesDelivered < totalBytes) { + PluginStopDeferrer deferrer(m_pluginView.get()); + int32_t deliveryBytes = m_pluginFuncs->writeready(m_plugin, &m_stream); + LOG(Plugins, "NPP_WriteReady responseURL=%@ bytes=%d", m_responseURL.get(), deliveryBytes); + + if (m_isTerminated) + return; + + if (deliveryBytes <= 0) { + // Plug-in can't receive anymore data right now. Send it later. + if (!m_deliverDataTimer.isActive()) + m_deliverDataTimer.startOneShot(0); + break; + } else { + deliveryBytes = min(deliveryBytes, totalBytes - totalBytesDelivered); + NSData *subdata = [m_deliveryData.get() subdataWithRange:NSMakeRange(totalBytesDelivered, deliveryBytes)]; + PluginStopDeferrer deferrer(m_pluginView.get()); + deliveryBytes = m_pluginFuncs->write(m_plugin, &m_stream, m_offset, [subdata length], (void *)[subdata bytes]); + if (deliveryBytes < 0) { + // Netscape documentation says that a negative result from NPP_Write means cancel the load. + cancelLoadAndDestroyStreamWithError(pluginCancelledConnectionError()); + return; + } + deliveryBytes = min(deliveryBytes, [subdata length]); + m_offset += deliveryBytes; + totalBytesDelivered += deliveryBytes; + LOG(Plugins, "NPP_Write responseURL=%@ bytes=%d total-delivered=%d/%d", m_responseURL.get(), deliveryBytes, m_offset, m_stream.end); + } + } + + if (totalBytesDelivered > 0) { + if (totalBytesDelivered < totalBytes) { + NSMutableData *newDeliveryData = [[NSMutableData alloc] initWithCapacity:totalBytes - totalBytesDelivered]; + [newDeliveryData appendBytes:(char *)[m_deliveryData.get() bytes] + totalBytesDelivered length:totalBytes - totalBytesDelivered]; + + m_deliveryData.adoptNS(newDeliveryData); + } else { + [m_deliveryData.get() setLength:0]; + if (m_reason != WEB_REASON_NONE) + destroyStream(); + } + } +} + +void WebNetscapePluginStream::deliverDataTimerFired(WebCore::Timer* timer) +{ + deliverData(); +} + +void WebNetscapePluginStream::deliverDataToFile(NSData *data) +{ + if (m_fileDescriptor == -1 && !m_path) { + NSString *temporaryFileMask = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WebKitPlugInStreamXXXXXX"]; + char *temporaryFileName = strdup([temporaryFileMask fileSystemRepresentation]); + m_fileDescriptor = mkstemp(temporaryFileName); + if (m_fileDescriptor == -1) { + LOG_ERROR("Can't create a temporary file."); + // This is not a network error, but the only error codes are "network error" and "user break". + destroyStreamWithReason(NPRES_NETWORK_ERR); + free(temporaryFileName); + return; + } + + m_path.adoptNS([[NSString stringWithUTF8String:temporaryFileName] retain]); + free(temporaryFileName); + } + + int dataLength = [data length]; + if (!dataLength) + return; + + int byteCount = write(m_fileDescriptor, [data bytes], dataLength); + if (byteCount != dataLength) { + // This happens only rarely, when we are out of disk space or have a disk I/O error. + LOG_ERROR("error writing to temporary file, errno %d", errno); + close(m_fileDescriptor); + m_fileDescriptor = -1; + + // This is not a network error, but the only error codes are "network error" and "user break". + destroyStreamWithReason(NPRES_NETWORK_ERR); + m_path = 0; + } +} + +void WebNetscapePluginStream::didFinishLoading(NetscapePlugInStreamLoader*) +{ + if (!m_stream.ndata) + return; + + if (m_transferMode == NP_ASFILE || m_transferMode == NP_ASFILEONLY) { + // Fake the delivery of an empty data to ensure that the file has been created + deliverDataToFile([NSData data]); + if (m_fileDescriptor != -1) + close(m_fileDescriptor); + m_fileDescriptor = -1; + } + + destroyStreamWithReason(NPRES_DONE); +} + +void WebNetscapePluginStream::didReceiveData(NetscapePlugInStreamLoader*, const char* bytes, int length) +{ + NSData *data = [[NSData alloc] initWithBytesNoCopy:(void*)bytes length:length freeWhenDone:NO]; + + ASSERT([data length] > 0); + + if (m_transferMode != NP_ASFILEONLY) { + if (!m_deliveryData) + m_deliveryData.adoptNS([[NSMutableData alloc] initWithCapacity:[data length]]); + [m_deliveryData.get() appendData:data]; + deliverData(); + } + if (m_transferMode == NP_ASFILE || m_transferMode == NP_ASFILEONLY) + deliverDataToFile(data); + + [data release]; +} + +static NSString *CarbonPathFromPOSIXPath(NSString *posixPath) +{ + // Doesn't add a trailing colon for directories; this is a problem for paths to a volume, + // so this function would need to be revised if we ever wanted to call it with that. + + CFURLRef url = (CFURLRef)[NSURL fileURLWithPath:posixPath]; + if (!url) + return nil; + + return WebCFAutorelease(CFURLCopyFileSystemPath(url, kCFURLHFSPathStyle)); +} + +#endif