/*
* Copyright (C) 2006, 2007, 2008 Apple Inc. All rights reserved.
* Copyright (C) 2008 Collabora, Ltd. 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.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``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 COMPUTER, INC. OR
* 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.
*/
#include "config.h"
#include "PluginStream.h"
#include "DocumentLoader.h"
#include "Frame.h"
#include "FrameLoader.h"
#include "PluginDebug.h"
#include "ResourceLoadScheduler.h"
#include "SharedBuffer.h"
#include "SubresourceLoader.h"
#include <wtf/StringExtras.h>
#include <wtf/text/CString.h>
#include <wtf/text/StringConcatenate.h>
// We use -2 here because some plugins like to return -1 to indicate error
// and this way we won't clash with them.
static const int WebReasonNone = -2;
using std::max;
using std::min;
namespace WebCore {
typedef HashMap<NPStream*, NPP> StreamMap;
static StreamMap& streams()
{
static StreamMap staticStreams;
return staticStreams;
}
PluginStream::PluginStream(PluginStreamClient* client, Frame* frame, const ResourceRequest& resourceRequest, bool sendNotification, void* notifyData, const NPPluginFuncs* pluginFuncs, NPP instance, const PluginQuirkSet& quirks)
: m_resourceRequest(resourceRequest)
, m_client(client)
, m_frame(frame)
, m_notifyData(notifyData)
, m_sendNotification(sendNotification)
, m_streamState(StreamBeforeStarted)
, m_loadManually(false)
, m_delayDeliveryTimer(this, &PluginStream::delayDeliveryTimerFired)
, m_deliveryData(0)
, m_tempFileHandle(invalidPlatformFileHandle)
, m_pluginFuncs(pluginFuncs)
, m_instance(instance)
, m_quirks(quirks)
{
ASSERT(m_instance);
m_stream.url = 0;
m_stream.ndata = 0;
m_stream.pdata = 0;
m_stream.end = 0;
m_stream.notifyData = 0;
m_stream.lastmodified = 0;
streams().add(&m_stream, m_instance);
}
PluginStream::~PluginStream()
{
ASSERT(m_streamState != StreamStarted);
ASSERT(!m_loader);
fastFree((char*)m_stream.url);
streams().remove(&m_stream);
}
void PluginStream::start()
{
ASSERT(!m_loadManually);
m_loader = resourceLoadScheduler()->schedulePluginStreamLoad(m_frame, this, m_resourceRequest);
}
void PluginStream::stop()
{
m_streamState = StreamStopped;
if (m_loadManually) {
ASSERT(!m_loader);
DocumentLoader* documentLoader = m_frame->loader()->activeDocumentLoader();
ASSERT(documentLoader);
if (documentLoader->isLoadingMainResource())
documentLoader->cancelMainResourceLoad(m_frame->loader()->cancelledError(m_resourceRequest));
return;
}
if (m_loader) {
m_loader->cancel();
m_loader = 0;
}
m_client = 0;
}
void PluginStream::startStream()
{
ASSERT(m_streamState == StreamBeforeStarted);
const KURL& responseURL = m_resourceResponse.url();
// Some plugins (Flash) expect that javascript URLs are passed back decoded as this is the
// format used when requesting the URL.
if (protocolIsJavaScript(responseURL))
m_stream.url = fastStrDup(decodeURLEscapeSequences(responseURL.string()).utf8().data());
else
m_stream.url = fastStrDup(responseURL.string().utf8().data());
CString mimeTypeStr = m_resourceResponse.mimeType().utf8();
long long expectedContentLength = m_resourceResponse.expectedContentLength();
if (m_resourceResponse.isHTTP()) {
Vector<UChar> stringBuilder;
String separator(": ");
String statusLine = makeString("HTTP ", String::number(m_resourceResponse.httpStatusCode()), " OK\n");
stringBuilder.append(statusLine.characters(), statusLine.length());
HTTPHeaderMap::const_iterator end = m_resourceResponse.httpHeaderFields().end();
for (HTTPHeaderMap::const_iterator it = m_resourceResponse.httpHeaderFields().begin(); it != end; ++it) {
stringBuilder.append(it->first.characters(), it->first.length());
stringBuilder.append(separator.characters(), separator.length());
stringBuilder.append(it->second.characters(), it->second.length());
stringBuilder.append('\n');
}
m_headers = String::adopt(stringBuilder).utf8();
// 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.
String contentEncoding = m_resourceResponse.httpHeaderField("Content-Encoding");
if (!contentEncoding.isNull() && contentEncoding != "identity")
expectedContentLength = -1;
}
m_stream.headers = m_headers.data();
m_stream.pdata = 0;
m_stream.ndata = this;
m_stream.end = max(expectedContentLength, 0LL);
m_stream.lastmodified = m_resourceResponse.lastModifiedDate();
m_stream.notifyData = m_notifyData;
m_transferMode = NP_NORMAL;
m_offset = 0;
m_reason = WebReasonNone;
// Protect the stream if destroystream is called from within the newstream handler
RefPtr<PluginStream> protect(this);
// calling into a plug-in could result in re-entrance if the plug-in yields
// control to the system (rdar://5744899). prevent this by deferring further
// loading while calling into the plug-in.
if (m_loader)
m_loader->setDefersLoading(true);
NPError npErr = m_pluginFuncs->newstream(m_instance, (NPMIMEType)mimeTypeStr.data(), &m_stream, false, &m_transferMode);
if (m_loader)
m_loader->setDefersLoading(false);
// If the stream was destroyed in the call to newstream we return
if (m_reason != WebReasonNone)
return;
if (npErr != NPERR_NO_ERROR) {
cancelAndDestroyStream(npErr);
return;
}
m_streamState = StreamStarted;
if (m_transferMode == NP_NORMAL)
return;
m_path = openTemporaryFile("WKP", m_tempFileHandle);
// Something went wrong, cancel loading the stream
if (!isHandleValid(m_tempFileHandle))
cancelAndDestroyStream(NPRES_NETWORK_ERR);
}
NPP PluginStream::ownerForStream(NPStream* stream)
{
return streams().get(stream);
}
void PluginStream::cancelAndDestroyStream(NPReason reason)
{
RefPtr<PluginStream> protect(this);
destroyStream(reason);
stop();
}
void PluginStream::destroyStream(NPReason reason)
{
m_reason = reason;
if (m_reason != NPRES_DONE) {
// Stop any pending data from being streamed
if (m_deliveryData)
m_deliveryData->resize(0);
} else if (m_deliveryData && m_deliveryData->size() > 0) {
// There is more data to be streamed, don't destroy the stream now.
return;
}
destroyStream();
}
void PluginStream::destroyStream()
{
if (m_streamState == StreamStopped)
return;
ASSERT(m_reason != WebReasonNone);
ASSERT(!m_deliveryData || m_deliveryData->size() == 0);
closeFile(m_tempFileHandle);
bool newStreamCalled = m_stream.ndata;
// Protect from destruction if:
// NPN_DestroyStream is called from NPP_NewStream or
// PluginStreamClient::streamDidFinishLoading() removes the last reference
RefPtr<PluginStream> protect(this);
if (newStreamCalled) {
if (m_reason == NPRES_DONE && (m_transferMode == NP_ASFILE || m_transferMode == NP_ASFILEONLY)) {
ASSERT(!m_path.isNull());
if (m_loader)
m_loader->setDefersLoading(true);
m_pluginFuncs->asfile(m_instance, &m_stream, m_path.utf8().data());
if (m_loader)
m_loader->setDefersLoading(false);
}
if (m_streamState != StreamBeforeStarted) {
if (m_loader)
m_loader->setDefersLoading(true);
NPError npErr = m_pluginFuncs->destroystream(m_instance, &m_stream, m_reason);
if (m_loader)
m_loader->setDefersLoading(false);
LOG_NPERROR(npErr);
}
m_stream.ndata = 0;
}
if (m_sendNotification) {
// Flash 9 can dereference null if we call NPP_URLNotify without first calling NPP_NewStream
// for requests made with NPN_PostURLNotify; see <rdar://5588807>
if (m_loader)
m_loader->setDefersLoading(true);
if (!newStreamCalled && m_quirks.contains(PluginQuirkFlashURLNotifyBug) &&
equalIgnoringCase(m_resourceRequest.httpMethod(), "POST")) {
m_transferMode = NP_NORMAL;
m_stream.url = "";
m_stream.notifyData = m_notifyData;
static char emptyMimeType[] = "";
m_pluginFuncs->newstream(m_instance, emptyMimeType, &m_stream, false, &m_transferMode);
m_pluginFuncs->destroystream(m_instance, &m_stream, m_reason);
// in successful requests, the URL is dynamically allocated and freed in our
// destructor, so reset it to 0
m_stream.url = 0;
}
m_pluginFuncs->urlnotify(m_instance, m_resourceRequest.url().string().utf8().data(), m_reason, m_notifyData);
if (m_loader)
m_loader->setDefersLoading(false);
}
m_streamState = StreamStopped;
if (!m_loadManually && m_client)
m_client->streamDidFinishLoading(this);
if (!m_path.isNull())
deleteFile(m_path);
}
void PluginStream::delayDeliveryTimerFired(Timer<PluginStream>* timer)
{
ASSERT(timer == &m_delayDeliveryTimer);
deliverData();
}
void PluginStream::deliverData()
{
ASSERT(m_deliveryData);
if (m_streamState == StreamStopped)
// FIXME: We should cancel our job in the SubresourceLoader on error so we don't reach this case
return;
ASSERT(m_streamState != StreamBeforeStarted);
if (!m_stream.ndata || m_deliveryData->size() == 0)
return;
int32_t totalBytes = m_deliveryData->size();
int32_t totalBytesDelivered = 0;
if (m_loader)
m_loader->setDefersLoading(true);
while (totalBytesDelivered < totalBytes) {
int32_t deliveryBytes = m_pluginFuncs->writeready(m_instance, &m_stream);
if (deliveryBytes <= 0) {
#if PLATFORM(ANDROID)
// TODO: This needs to be upstreamed.
if (m_loader)
m_loader->pauseLoad(true);
// ask the plugin for a delay value.
int delay = deliveryDelay();
m_delayDeliveryTimer.startOneShot(delay * 0.001);
#else
m_delayDeliveryTimer.startOneShot(0);
#endif
break;
} else {
deliveryBytes = min(deliveryBytes, totalBytes - totalBytesDelivered);
int32_t dataLength = deliveryBytes;
char* data = m_deliveryData->data() + totalBytesDelivered;
// Write the data
deliveryBytes = m_pluginFuncs->write(m_instance, &m_stream, m_offset, dataLength, (void*)data);
if (deliveryBytes < 0) {
LOG_PLUGIN_NET_ERROR();
if (m_loader)
m_loader->setDefersLoading(false);
cancelAndDestroyStream(NPRES_NETWORK_ERR);
return;
}
deliveryBytes = min(deliveryBytes, dataLength);
m_offset += deliveryBytes;
totalBytesDelivered += deliveryBytes;
}
}
if (m_loader)
m_loader->setDefersLoading(false);
if (totalBytesDelivered > 0) {
if (totalBytesDelivered < totalBytes) {
int remainingBytes = totalBytes - totalBytesDelivered;
memmove(m_deliveryData->data(), m_deliveryData->data() + totalBytesDelivered, remainingBytes);
m_deliveryData->resize(remainingBytes);
} else {
#if PLATFORM(ANDROID)
//TODO: This needs to be upstreamed to WebKit.
if (m_loader)
m_loader->pauseLoad(false);
#endif
m_deliveryData->resize(0);
if (m_reason != WebReasonNone)
destroyStream();
}
}
}
void PluginStream::sendJavaScriptStream(const KURL& requestURL, const CString& resultString)
{
didReceiveResponse(0, ResourceResponse(requestURL, "text/plain", resultString.length(), "", ""));
if (m_streamState == StreamStopped)
return;
if (!resultString.isNull()) {
didReceiveData(0, resultString.data(), resultString.length());
if (m_streamState == StreamStopped)
return;
}
m_loader = 0;
destroyStream(resultString.isNull() ? NPRES_NETWORK_ERR : NPRES_DONE);
}
void PluginStream::didReceiveResponse(NetscapePlugInStreamLoader* loader, const ResourceResponse& response)
{
ASSERT(loader == m_loader);
ASSERT(m_streamState == StreamBeforeStarted);
m_resourceResponse = response;
startStream();
}
void PluginStream::didReceiveData(NetscapePlugInStreamLoader* loader, const char* data, int length)
{
ASSERT(loader == m_loader);
ASSERT(m_streamState == StreamStarted);
// If the plug-in cancels the stream in deliverData it could be deleted,
// so protect it here.
RefPtr<PluginStream> protect(this);
if (m_transferMode != NP_ASFILEONLY) {
if (!m_deliveryData)
m_deliveryData.set(new Vector<char>);
int oldSize = m_deliveryData->size();
m_deliveryData->resize(oldSize + length);
memcpy(m_deliveryData->data() + oldSize, data, length);
#if PLATFORM(ANDROID)
//TODO: This needs to be upstreamed to WebKit.
if (!m_delayDeliveryTimer.isActive())
#endif
deliverData();
}
if (m_streamState != StreamStopped && isHandleValid(m_tempFileHandle)) {
int bytesWritten = writeToFile(m_tempFileHandle, data, length);
if (bytesWritten != length)
cancelAndDestroyStream(NPRES_NETWORK_ERR);
}
}
void PluginStream::didFail(NetscapePlugInStreamLoader* loader, const ResourceError&)
{
ASSERT(loader == m_loader);
LOG_PLUGIN_NET_ERROR();
// destroyStream can result in our being deleted
RefPtr<PluginStream> protect(this);
destroyStream(NPRES_NETWORK_ERR);
m_loader = 0;
}
void PluginStream::didFinishLoading(NetscapePlugInStreamLoader* loader)
{
ASSERT(loader == m_loader);
ASSERT(m_streamState == StreamStarted);
// destroyStream can result in our being deleted
RefPtr<PluginStream> protect(this);
destroyStream(NPRES_DONE);
m_loader = 0;
}
bool PluginStream::wantsAllStreams() const
{
if (!m_pluginFuncs->getvalue)
return false;
void* result = 0;
if (m_pluginFuncs->getvalue(m_instance, NPPVpluginWantsAllNetworkStreams, &result) != NPERR_NO_ERROR)
return false;
return result != 0;
}
#if PLATFORM(ANDROID)
int PluginStream::deliveryDelay() const
{
if (!m_pluginFuncs->getvalue)
return 0;
int delay = 0;
if (m_pluginFuncs->getvalue(m_instance, NPPDataDeliveryDelayMs, &delay) != NPERR_NO_ERROR)
return 0;
return delay;
}
#endif
}