/* * 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 <Foundation/NSURLResponse.h> #import <runtime/JSLock.h> #import <WebCore/DocumentLoader.h> #import <WebCore/Frame.h> #import <WebCore/FrameLoader.h> #import <WebCore/ResourceLoadScheduler.h> #import <WebCore/SecurityOrigin.h> #import <WebCore/WebCoreObjCExtras.h> #import <WebCore/WebCoreURLResponse.h> #import <WebKitSystemInterface.h> #import <wtf/HashMap.h> #import <wtf/StdLibExtras.h> 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<WebNetscapePluginView> m_pluginView; }; typedef HashMap<NPStream*, NPP> 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(core([view webFrame])->document()->securityOrigin()->canDisplay([request URL])); 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<WebNetscapePluginView *>(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 = resourceLoadScheduler()->schedulePluginStreamLoad(core([m_pluginView.get() webFrame]), this, 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. // <rdar://problem/4470599> 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<WebNetscapePluginStream> 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<WebNetscapePluginStream> 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<WebNetscapePluginStream> protect(this); cancelLoadWithError(error); destroyStreamWithError(error); setPlugin(0); } void WebNetscapePluginStream::deliverData() { if (!m_stream.ndata || [m_deliveryData.get() length] == 0) return; RefPtr<WebNetscapePluginStream> 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<int32_t>(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<WebNetscapePluginStream>* 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