1/*
2 * Copyright (C) 2010 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "NetscapePluginStream.h"
28
29#if ENABLE(NETSCAPE_PLUGIN_API)
30
31#include "NetscapePlugin.h"
32#include <utility>
33#include <wtf/Vector.h>
34
35using namespace WebCore;
36
37namespace WebKit {
38
39NetscapePluginStream::NetscapePluginStream(PassRefPtr<NetscapePlugin> plugin, uint64_t streamID, const String& requestURLString, bool sendNotification, void* notificationData)
40    : m_plugin(plugin)
41    , m_streamID(streamID)
42    , m_requestURLString(requestURLString)
43    , m_sendNotification(sendNotification)
44    , m_notificationData(notificationData)
45    , m_npStream()
46    , m_transferMode(NP_NORMAL)
47    , m_offset(0)
48    , m_fileHandle(invalidPlatformFileHandle)
49    , m_isStarted(false)
50#if !ASSERT_DISABLED
51    , m_urlNotifyHasBeenCalled(false)
52#endif
53    , m_deliveryDataTimer(RunLoop::main(), this, &NetscapePluginStream::deliverDataToPlugin)
54    , m_stopStreamWhenDoneDelivering(false)
55{
56}
57
58NetscapePluginStream::~NetscapePluginStream()
59{
60    ASSERT(!m_isStarted);
61    ASSERT(!m_sendNotification || m_urlNotifyHasBeenCalled);
62    ASSERT(m_fileHandle == invalidPlatformFileHandle);
63}
64
65void NetscapePluginStream::didReceiveResponse(const URL& responseURL, uint32_t streamLength, uint32_t lastModifiedTime, const String& mimeType, const String& headers)
66{
67    // Starting the stream could cause the plug-in stream to go away so we keep a reference to it here.
68    Ref<NetscapePluginStream> protect(*this);
69
70    start(responseURL, streamLength, lastModifiedTime, mimeType, headers);
71}
72
73void NetscapePluginStream::didReceiveData(const char* bytes, int length)
74{
75    // Delivering the data could cause the plug-in stream to go away so we keep a reference to it here.
76    Ref<NetscapePluginStream> protect(*this);
77
78    deliverData(bytes, length);
79}
80
81void NetscapePluginStream::didFinishLoading()
82{
83    // Stopping the stream could cause the plug-in stream to go away so we keep a reference to it here.
84    Ref<NetscapePluginStream> protect(*this);
85
86    stop(NPRES_DONE);
87}
88
89void NetscapePluginStream::didFail(bool wasCancelled)
90{
91    // Stopping the stream could cause the plug-in stream to go away so we keep a reference to it here.
92    Ref<NetscapePluginStream> protect(*this);
93
94    stop(wasCancelled ? NPRES_USER_BREAK : NPRES_NETWORK_ERR);
95}
96
97void NetscapePluginStream::sendJavaScriptStream(const String& result)
98{
99    // starting the stream or delivering the data to it might cause the plug-in stream to go away, so we keep
100    // a reference to it here.
101    Ref<NetscapePluginStream> protect(*this);
102
103    CString resultCString = result.utf8();
104    if (resultCString.isNull()) {
105        // There was an error evaluating the JavaScript, call NPP_URLNotify if needed and then destroy the stream.
106        notifyAndDestroyStream(NPRES_NETWORK_ERR);
107        return;
108    }
109
110    if (!start(m_requestURLString, resultCString.length(), 0, "text/plain", ""))
111        return;
112
113    deliverData(resultCString.data(), resultCString.length());
114    stop(NPRES_DONE);
115}
116
117NPError NetscapePluginStream::destroy(NPReason reason)
118{
119    // It doesn't make sense to call NPN_DestroyStream on a stream that hasn't been started yet.
120    if (!m_isStarted)
121        return NPERR_GENERIC_ERROR;
122
123    // It isn't really valid for a plug-in to call NPN_DestroyStream with NPRES_DONE.
124    // (At least not for browser initiated streams, and we don't support plug-in initiated streams).
125    if (reason == NPRES_DONE)
126        return NPERR_INVALID_PARAM;
127
128    cancel();
129    stop(reason);
130    return NPERR_NO_ERROR;
131}
132
133static bool isSupportedTransferMode(uint16_t transferMode)
134{
135    switch (transferMode) {
136    case NP_ASFILEONLY:
137    case NP_ASFILE:
138    case NP_NORMAL:
139        return true;
140    // FIXME: We don't support seekable streams.
141    case NP_SEEK:
142        return false;
143    }
144
145    ASSERT_NOT_REACHED();
146    return false;
147}
148
149bool NetscapePluginStream::start(const String& responseURLString, uint32_t streamLength, uint32_t lastModifiedTime, const String& mimeType, const String& headers)
150{
151    m_responseURL = responseURLString.utf8();
152    m_mimeType = mimeType.utf8();
153    m_headers = headers.utf8();
154
155    m_npStream.ndata = this;
156    m_npStream.url = m_responseURL.data();
157    m_npStream.end = streamLength;
158    m_npStream.lastmodified = lastModifiedTime;
159    m_npStream.notifyData = m_notificationData;
160    m_npStream.headers = m_headers.length() == 0 ? 0 : m_headers.data();
161
162    NPError error = m_plugin->NPP_NewStream(const_cast<char*>(m_mimeType.data()), &m_npStream, false, &m_transferMode);
163    if (error != NPERR_NO_ERROR) {
164        // We failed to start the stream, cancel the load and destroy it.
165        cancel();
166        notifyAndDestroyStream(NPRES_NETWORK_ERR);
167        return false;
168    }
169
170    // We successfully started the stream.
171    m_isStarted = true;
172
173    if (!isSupportedTransferMode(m_transferMode)) {
174        // Cancel the load and stop the stream.
175        cancel();
176        stop(NPRES_NETWORK_ERR);
177        return false;
178    }
179
180    return true;
181}
182
183void NetscapePluginStream::deliverData(const char* bytes, int length)
184{
185    ASSERT(m_isStarted);
186
187    if (m_transferMode != NP_ASFILEONLY) {
188        if (!m_deliveryData)
189            m_deliveryData = std::make_unique<Vector<uint8_t>>();
190
191        m_deliveryData->reserveCapacity(m_deliveryData->size() + length);
192        m_deliveryData->append(bytes, length);
193
194        deliverDataToPlugin();
195    }
196
197    if (m_transferMode == NP_ASFILE || m_transferMode == NP_ASFILEONLY)
198        deliverDataToFile(bytes, length);
199}
200
201void NetscapePluginStream::deliverDataToPlugin()
202{
203    ASSERT(m_isStarted);
204
205    int32_t numBytesToDeliver = m_deliveryData->size();
206    int32_t numBytesDelivered = 0;
207
208    while (numBytesDelivered < numBytesToDeliver) {
209        int32_t numBytesPluginCanHandle = m_plugin->NPP_WriteReady(&m_npStream);
210
211        // NPP_WriteReady could call NPN_DestroyStream and destroy the stream.
212        if (!m_isStarted)
213            return;
214
215        if (numBytesPluginCanHandle <= 0) {
216            // The plug-in can't handle more data, we'll send the rest later
217            m_deliveryDataTimer.startOneShot(0);
218            break;
219        }
220
221        // Figure out how much data to send to the plug-in.
222        int32_t dataLength = std::min(numBytesPluginCanHandle, numBytesToDeliver - numBytesDelivered);
223        uint8_t* data = m_deliveryData->data() + numBytesDelivered;
224
225        int32_t numBytesWritten = m_plugin->NPP_Write(&m_npStream, m_offset, dataLength, data);
226        if (numBytesWritten < 0) {
227            cancel();
228            stop(NPRES_NETWORK_ERR);
229            return;
230        }
231
232        // NPP_Write could call NPN_DestroyStream and destroy the stream.
233        if (!m_isStarted)
234            return;
235
236        numBytesWritten = std::min(numBytesWritten, dataLength);
237        m_offset += numBytesWritten;
238        numBytesDelivered += numBytesWritten;
239    }
240
241    // We didn't write anything.
242    if (!numBytesDelivered)
243        return;
244
245    if (numBytesDelivered < numBytesToDeliver) {
246        // Remove the bytes that we actually delivered.
247        m_deliveryData->remove(0, numBytesDelivered);
248    } else {
249        m_deliveryData->clear();
250
251        if (m_stopStreamWhenDoneDelivering)
252            stop(NPRES_DONE);
253    }
254}
255
256void NetscapePluginStream::deliverDataToFile(const char* bytes, int length)
257{
258    if (m_fileHandle == invalidPlatformFileHandle && m_filePath.isNull()) {
259        // Create a temporary file.
260        m_filePath = openTemporaryFile("WebKitPluginStream", m_fileHandle);
261
262        // We failed to open the file, stop the stream.
263        if (m_fileHandle == invalidPlatformFileHandle) {
264            stop(NPRES_NETWORK_ERR);
265            return;
266        }
267    }
268
269    if (!length)
270        return;
271
272    int byteCount = writeToFile(m_fileHandle, bytes, length);
273    if (byteCount != length) {
274        // This happens only rarely, when we are out of disk space or have a disk I/O error.
275        closeFile(m_fileHandle);
276
277        stop(NPRES_NETWORK_ERR);
278    }
279}
280
281void NetscapePluginStream::stop(NPReason reason)
282{
283    // The stream was stopped before it got a chance to start. This can happen if a stream is cancelled by
284    // WebKit before it received a response.
285    if (!m_isStarted) {
286        ASSERT(reason != NPRES_DONE);
287        notifyAndDestroyStream(reason);
288        return;
289    }
290
291    if (reason == NPRES_DONE && m_deliveryData && !m_deliveryData->isEmpty()) {
292        // There is still data left that the plug-in hasn't been able to consume yet.
293        ASSERT(m_deliveryDataTimer.isActive());
294
295        // Set m_stopStreamWhenDoneDelivering to true so that the next time the delivery timer fires
296        // and calls deliverDataToPlugin the stream will be closed if all the remaining data was
297        // successfully delivered.
298        m_stopStreamWhenDoneDelivering = true;
299        return;
300    }
301
302    m_deliveryData = nullptr;
303    m_deliveryDataTimer.stop();
304
305    if (m_transferMode == NP_ASFILE || m_transferMode == NP_ASFILEONLY) {
306        if (reason == NPRES_DONE) {
307            // Ensure that the file is created.
308            deliverDataToFile(0, 0);
309            if (m_fileHandle != invalidPlatformFileHandle)
310                closeFile(m_fileHandle);
311
312            ASSERT(!m_filePath.isNull());
313
314            m_plugin->NPP_StreamAsFile(&m_npStream, m_filePath.utf8().data());
315        } else {
316            // Just close the file.
317            if (m_fileHandle != invalidPlatformFileHandle)
318                closeFile(m_fileHandle);
319        }
320
321        // Delete the file after calling NPP_StreamAsFile(), instead of in the destructor.  It should be OK
322        // to delete the file here -- NPP_StreamAsFile() is always called immediately before NPP_DestroyStream()
323        // (the stream destruction function), so there can be no expectation that a plugin will read the stream
324        // file asynchronously after NPP_StreamAsFile() is called.
325        deleteFile(m_filePath);
326        m_filePath = String();
327
328        // NPP_StreamAsFile could call NPN_DestroyStream and destroy the stream.
329        if (!m_isStarted)
330            return;
331    }
332
333    // Set m_isStarted to false before calling NPP_DestroyStream in case NPP_DestroyStream calls NPN_DestroyStream.
334    m_isStarted = false;
335
336    m_plugin->NPP_DestroyStream(&m_npStream, reason);
337
338    notifyAndDestroyStream(reason);
339}
340
341void NetscapePluginStream::cancel()
342{
343    m_plugin->cancelStreamLoad(this);
344}
345
346void NetscapePluginStream::notifyAndDestroyStream(NPReason reason)
347{
348    ASSERT(!m_isStarted);
349    ASSERT(!m_deliveryDataTimer.isActive());
350    ASSERT(!m_urlNotifyHasBeenCalled);
351
352    if (m_sendNotification) {
353        m_plugin->NPP_URLNotify(m_requestURLString.utf8().data(), reason, m_notificationData);
354
355#if !ASSERT_DISABLED
356        m_urlNotifyHasBeenCalled = true;
357#endif
358    }
359
360    m_plugin->removePluginStream(this);
361}
362
363} // namespace WebKit
364
365#endif // ENABLE(NETSCAPE_PLUGIN_API)
366