/*
* Copyright (C) 2010 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.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. 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 INC. 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.
*/
#include "config.h"
#include "NetscapePluginStream.h"
#include "NetscapePlugin.h"
#include <utility>
using namespace WebCore;
using namespace std;
namespace WebKit {
NetscapePluginStream::NetscapePluginStream(PassRefPtr<NetscapePlugin> plugin, uint64_t streamID, bool sendNotification, void* notificationData)
: m_plugin(plugin)
, m_streamID(streamID)
, m_sendNotification(sendNotification)
, m_notificationData(notificationData)
, m_npStream()
, m_transferMode(NP_NORMAL)
, m_offset(0)
, m_fileHandle(invalidPlatformFileHandle)
, m_isStarted(false)
#if !ASSERT_DISABLED
, m_urlNotifyHasBeenCalled(false)
#endif
, m_deliveryDataTimer(RunLoop::main(), this, &NetscapePluginStream::deliverDataToPlugin)
, m_stopStreamWhenDoneDelivering(false)
{
}
NetscapePluginStream::~NetscapePluginStream()
{
ASSERT(!m_isStarted);
ASSERT(!m_sendNotification || m_urlNotifyHasBeenCalled);
ASSERT(m_fileHandle == invalidPlatformFileHandle);
}
void NetscapePluginStream::didReceiveResponse(const KURL& responseURL, uint32_t streamLength, uint32_t lastModifiedTime, const String& mimeType, const String& headers)
{
// Starting the stream could cause the plug-in stream to go away so we keep a reference to it here.
RefPtr<NetscapePluginStream> protect(this);
start(responseURL, streamLength, lastModifiedTime, mimeType, headers);
}
void NetscapePluginStream::didReceiveData(const char* bytes, int length)
{
// Delivering the data could cause the plug-in stream to go away so we keep a reference to it here.
RefPtr<NetscapePluginStream> protect(this);
deliverData(bytes, length);
}
void NetscapePluginStream::didFinishLoading()
{
// Stopping the stream could cause the plug-in stream to go away so we keep a reference to it here.
RefPtr<NetscapePluginStream> protect(this);
stop(NPRES_DONE);
}
void NetscapePluginStream::didFail(bool wasCancelled)
{
// Stopping the stream could cause the plug-in stream to go away so we keep a reference to it here.
RefPtr<NetscapePluginStream> protect(this);
stop(wasCancelled ? NPRES_USER_BREAK : NPRES_NETWORK_ERR);
}
void NetscapePluginStream::sendJavaScriptStream(const String& requestURLString, const String& result)
{
// starting the stream or delivering the data to it might cause the plug-in stream to go away, so we keep
// a reference to it here.
RefPtr<NetscapePluginStream> protect(this);
CString resultCString = requestURLString.utf8();
if (resultCString.isNull()) {
// There was an error evaluating the JavaScript, call NPP_URLNotify if needed and then destroy the stream.
notifyAndDestroyStream(NPRES_NETWORK_ERR);
return;
}
if (!start(requestURLString, resultCString.length(), 0, "text/plain", ""))
return;
deliverData(resultCString.data(), resultCString.length());
stop(NPRES_DONE);
}
NPError NetscapePluginStream::destroy(NPReason reason)
{
// It doesn't make sense to call NPN_DestroyStream on a stream that hasn't been started yet.
if (!m_isStarted)
return NPERR_GENERIC_ERROR;
// It isn't really valid for a plug-in to call NPN_DestroyStream with NPRES_DONE.
// (At least not for browser initiated streams, and we don't support plug-in initiated streams).
if (reason == NPRES_DONE)
return NPERR_INVALID_PARAM;
cancel();
stop(reason);
return NPERR_NO_ERROR;
}
static bool isSupportedTransferMode(uint16_t transferMode)
{
switch (transferMode) {
case NP_ASFILEONLY:
case NP_ASFILE:
case NP_NORMAL:
return true;
// FIXME: We don't support seekable streams.
case NP_SEEK:
return false;
}
ASSERT_NOT_REACHED();
return false;
}
bool NetscapePluginStream::start(const String& responseURLString, uint32_t streamLength, uint32_t lastModifiedTime, const String& mimeType, const String& headers)
{
m_responseURL = responseURLString.utf8();
m_mimeType = mimeType.utf8();
m_headers = headers.utf8();
m_npStream.ndata = this;
m_npStream.url = m_responseURL.data();
m_npStream.end = streamLength;
m_npStream.lastmodified = lastModifiedTime;
m_npStream.notifyData = m_notificationData;
m_npStream.headers = m_headers.length() == 0 ? 0 : m_headers.data();
NPError error = m_plugin->NPP_NewStream(const_cast<char*>(m_mimeType.data()), &m_npStream, false, &m_transferMode);
if (error != NPERR_NO_ERROR) {
// We failed to start the stream, cancel the load and destroy it.
cancel();
notifyAndDestroyStream(NPRES_NETWORK_ERR);
return false;
}
// We successfully started the stream.
m_isStarted = true;
if (!isSupportedTransferMode(m_transferMode)) {
// Cancel the load and stop the stream.
cancel();
stop(NPRES_NETWORK_ERR);
return false;
}
return true;
}
void NetscapePluginStream::deliverData(const char* bytes, int length)
{
ASSERT(m_isStarted);
if (m_transferMode != NP_ASFILEONLY) {
if (!m_deliveryData)
m_deliveryData.set(new Vector<uint8_t>);
m_deliveryData->reserveCapacity(m_deliveryData->size() + length);
m_deliveryData->append(bytes, length);
deliverDataToPlugin();
}
if (m_transferMode == NP_ASFILE || m_transferMode == NP_ASFILEONLY)
deliverDataToFile(bytes, length);
}
void NetscapePluginStream::deliverDataToPlugin()
{
ASSERT(m_isStarted);
int32_t numBytesToDeliver = m_deliveryData->size();
int32_t numBytesDelivered = 0;
while (numBytesDelivered < numBytesToDeliver) {
int32_t numBytesPluginCanHandle = m_plugin->NPP_WriteReady(&m_npStream);
// NPP_WriteReady could call NPN_DestroyStream and destroy the stream.
if (!m_isStarted)
return;
if (numBytesPluginCanHandle <= 0) {
// The plug-in can't handle more data, we'll send the rest later
m_deliveryDataTimer.startOneShot(0);
break;
}
// Figure out how much data to send to the plug-in.
int32_t dataLength = min(numBytesPluginCanHandle, numBytesToDeliver - numBytesDelivered);
uint8_t* data = m_deliveryData->data() + numBytesDelivered;
int32_t numBytesWritten = m_plugin->NPP_Write(&m_npStream, m_offset, dataLength, data);
if (numBytesWritten < 0) {
cancel();
stop(NPRES_NETWORK_ERR);
return;
}
// NPP_Write could call NPN_DestroyStream and destroy the stream.
if (!m_isStarted)
return;
numBytesWritten = min(numBytesWritten, dataLength);
m_offset += numBytesWritten;
numBytesDelivered += numBytesWritten;
}
// We didn't write anything.
if (!numBytesDelivered)
return;
if (numBytesDelivered < numBytesToDeliver) {
// Remove the bytes that we actually delivered.
m_deliveryData->remove(0, numBytesDelivered);
} else {
m_deliveryData->clear();
if (m_stopStreamWhenDoneDelivering)
stop(NPRES_DONE);
}
}
void NetscapePluginStream::deliverDataToFile(const char* bytes, int length)
{
if (m_fileHandle == invalidPlatformFileHandle && m_filePath.isNull()) {
// Create a temporary file.
m_filePath = openTemporaryFile("WebKitPluginStream", m_fileHandle);
// We failed to open the file, stop the stream.
if (m_fileHandle == invalidPlatformFileHandle) {
stop(NPRES_NETWORK_ERR);
return;
}
}
if (!length)
return;
int byteCount = writeToFile(m_fileHandle, bytes, length);
if (byteCount != length) {
// This happens only rarely, when we are out of disk space or have a disk I/O error.
closeFile(m_fileHandle);
stop(NPRES_NETWORK_ERR);
}
}
void NetscapePluginStream::stop(NPReason reason)
{
// The stream was stopped before it got a chance to start. This can happen if a stream is cancelled by
// WebKit before it received a response.
if (!m_isStarted)
return;
if (reason == NPRES_DONE && m_deliveryData && !m_deliveryData->isEmpty()) {
// There is still data left that the plug-in hasn't been able to consume yet.
ASSERT(m_deliveryDataTimer.isActive());
// Set m_stopStreamWhenDoneDelivering to true so that the next time the delivery timer fires
// and calls deliverDataToPlugin the stream will be closed if all the remaining data was
// successfully delivered.
m_stopStreamWhenDoneDelivering = true;
return;
}
m_deliveryData = 0;
m_deliveryDataTimer.stop();
if (m_transferMode == NP_ASFILE || m_transferMode == NP_ASFILEONLY) {
if (reason == NPRES_DONE) {
// Ensure that the file is created.
deliverDataToFile(0, 0);
if (m_fileHandle != invalidPlatformFileHandle)
closeFile(m_fileHandle);
ASSERT(!m_filePath.isNull());
m_plugin->NPP_StreamAsFile(&m_npStream, m_filePath.utf8().data());
} else {
// Just close the file.
if (m_fileHandle != invalidPlatformFileHandle)
closeFile(m_fileHandle);
}
// Delete the file after calling NPP_StreamAsFile(), instead of in the destructor. 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.
deleteFile(m_filePath);
m_filePath = String();
// NPP_StreamAsFile could call NPN_DestroyStream and destroy the stream.
if (!m_isStarted)
return;
}
// Set m_isStarted to false before calling NPP_DestroyStream in case NPP_DestroyStream calls NPN_DestroyStream.
m_isStarted = false;
m_plugin->NPP_DestroyStream(&m_npStream, reason);
notifyAndDestroyStream(reason);
}
void NetscapePluginStream::cancel()
{
m_plugin->cancelStreamLoad(this);
}
void NetscapePluginStream::notifyAndDestroyStream(NPReason reason)
{
ASSERT(!m_isStarted);
ASSERT(!m_deliveryDataTimer.isActive());
ASSERT(!m_urlNotifyHasBeenCalled);
if (m_sendNotification) {
m_plugin->NPP_URLNotify(m_responseURL.data(), reason, m_notificationData);
#if !ASSERT_DISABLED
m_urlNotifyHasBeenCalled = true;
#endif
}
m_plugin->removePluginStream(this);
}
} // namespace WebKit