blob: b18c3c9650b4c466fc93f85f06d196584bb02b3f [file] [log] [blame]
// Copyright 2009, Google Inc. All rights reserved.
// Portions of this file were adapted from the Mozilla project.
// See https://developer.mozilla.org/en/ActiveX_Control_for_Hosting_Netscape_Plug-ins_in_IE
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Netscape security libraries.
*
* The Initial Developer of the Original Code is
* Netscape Communications Corporation.
* Portions created by the Initial Developer are Copyright (C) 1994-2000
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Adam Lock <adamlock@eircom.net>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
#include <wininet.h>
#include "plugin/npapi_host_control/win/stream_operation.h"
#include "plugin/npapi_host_control/win/host_control.h"
#include "plugin/npapi_host_control/win/np_plugin_proxy.h"
namespace {
// The following classes are used to package arguments for interacting with
// the hosted NPAPI plug-in.
struct NPPDestroyStreamArgs {
NPP npp_;
NPStream *stream_;
NPReason reason_;
NPError *return_code_;
};
struct NPPNewStreamArgs {
NPP npp_;
NPMIMEType type_;
NPStream *stream_;
NPBool seekable_;
uint16 *stype_;
NPError *return_code_;
};
struct NPPAsFileArgs {
NPP npp_;
NPStream *stream_;
const char* fname_;
};
struct NPPUrlNotifyArgs {
NPP npp_;
const char* url_;
NPReason reason_;
void *notify_data_;
};
struct NPPWriteReadyArgs {
NPP npp_;
NPStream *stream_;
int32 *return_value_;
};
struct NPPWriteArgs {
NPP npp_;
NPStream *stream_;
int32 offset_;
int32 len_;
void* buffer_;
int32 *return_value_;
};
// Helper function that constructs the full url from a moniker associated with
// a base 'left-side' url prefix, and a stream operation.
HRESULT ConstructFullURLPath(const StreamOperation& stream_operation,
IMoniker* base_moniker,
CString* out_string) {
ATLASSERT(base_moniker && out_string);
HRESULT hr = S_OK;
CComPtr<IMoniker> full_url_moniker;
if (FAILED(hr = CreateURLMonikerEx(base_moniker,
stream_operation.GetURL(),
&full_url_moniker,
URL_MK_UNIFORM))) {
return hr;
}
// Determine if the monikers share a common prefix. If they do, then
// we can allow the data fetch to proceed - The same origin criteria has been
// satisfied.
CComPtr<IMoniker> prefix_moniker;
bool urls_contain_prefix = false;
hr = MonikerCommonPrefixWith(base_moniker, full_url_moniker, &prefix_moniker);
if (SUCCEEDED(hr)) {
urls_contain_prefix = true;
}
CComPtr<IBindCtx> bind_context;
if (FAILED(hr = CreateBindCtx(0, &bind_context))) {
return hr;
}
CComPtr<IMalloc> malloc_interface;
if (FAILED(hr = CoGetMalloc(1, &malloc_interface))) {
return hr;
}
LPOLESTR full_url_path = NULL;
if (FAILED(hr = full_url_moniker->GetDisplayName(bind_context,
NULL,
&full_url_path))) {
return hr;
}
if (!urls_contain_prefix) {
// If the urls do not contain a common prefix, validate the access request
// based on the fully qualified uri's.
LPOLESTR base_path_name = NULL;
if (FAILED(hr = base_moniker->GetDisplayName(bind_context,
NULL,
&base_path_name))) {
malloc_interface->Free(full_url_path);
return hr;
}
}
*out_string = full_url_path;
malloc_interface->Free(full_url_path);
return S_OK;
}
// Helper routine implementing a custom version of SendMessage(...).
// The StreamingOperation class uses windows messages to communicate transfer
// notifications to the plug-in. This is required so that the plug-in will
// receive notifications synchronously on the main-browser thread.
// SendMessage is not appropriate, because messages 'sent' to a window are NOT
// processed during DispatchMessage, but instead during GetMessage, PeekMessage
// and others. Because the JScript engine periodically peeks the message
// queue during JavaScript evaluation, the plug-in would be notified, and
// potentially call back into the JavaScript environment causing unexpected
// reentrancy.
HRESULT CustomSendMessage(HWND window_handle, UINT message, LPARAM l_param) {
// Mimic the behaviour of SendMessage by posting to the window, and then
// blocking on an event. Note that the message handlers must set
// the event.
HANDLE local_event = CreateEvent(NULL, TRUE, FALSE, NULL);
if (!local_event) {
return E_FAIL;
}
if (!PostMessage(window_handle, message,
reinterpret_cast<WPARAM>(&local_event), l_param)) {
CloseHandle(local_event);
return E_FAIL;
}
HRESULT hr;
static const unsigned int kWaitTimeOut = 120000;
bool done = false;
while (!done) {
DWORD wait_code = MsgWaitForMultipleObjects(1,
&local_event,
FALSE,
kWaitTimeOut,
QS_ALLINPUT);
switch (wait_code) {
case WAIT_OBJECT_0:
hr = S_OK;
done = true;
break;
case WAIT_OBJECT_0 + 1:
MSG msg;
GetMessage(&msg, NULL, 0, 0);
TranslateMessage(&msg);
DispatchMessage(&msg);
break;
case WAIT_TIMEOUT:
// If the plug-in is busy processing JavaScript code, it's possible
// that we may time-out here. We don't break out of the loop, because
// the event will eventually be signaled when the JS is done
// processing.
ATLASSERT(false && "Time out waiting for response from main thread.");
break;
default:
ATLASSERT(false &&
"Critical failure waiting for response from main thread.");
hr = E_FAIL;
done = true;
break;
}
}
CloseHandle(local_event);
return hr;
}
} // unnamed namespace
StreamOperation::StreamOperation()
: stream_size_(0),
stream_received_(0),
stream_type_(NP_NORMAL),
temp_file_(NULL),
thread_handle_(NULL),
cancel_requested_(false) {
memset(&np_stream_, 0, sizeof(np_stream_));
}
StreamOperation::~StreamOperation() {
}
void StreamOperation::OnFinalMessage(HWND hWnd) {
CWindowImplBase::OnFinalMessage(hWnd);
if (owner_) {
owner_->UnregisterStreamOperation(this);
}
// The binding holds a reference to the stream operation, which forms
// a cyclic reference chain. Release the binding so that both objects
// can be destroyed.
binding_ = NULL;
// The object has an artificially boosted reference count to ensure that it
// stays alive as long as it may receive window messages. Release this
// reference here, and potentially free the instance.
Release();
}
HRESULT STDMETHODCALLTYPE StreamOperation::OnStartBinding(DWORD dwReserved,
IBinding *pib) {
binding_ = pib;
return S_OK;
}
HRESULT STDMETHODCALLTYPE StreamOperation::GetPriority(LONG *pnPriority) {
return E_NOTIMPL;
}
HRESULT STDMETHODCALLTYPE StreamOperation::OnLowResource(DWORD reserved) {
return S_OK;
}
HRESULT STDMETHODCALLTYPE StreamOperation::OnProgress(ULONG ulProgress,
ULONG ulProgressMax,
ULONG ulStatusCode,
LPCWSTR szStatusText) {
if (cancel_requested_) {
binding_->Abort();
return S_OK;
}
// Capture URL re-directs and MIME-type status notifications.
switch (ulStatusCode) {
case BINDSTATUS_BEGINDOWNLOADDATA:
case BINDSTATUS_REDIRECTING:
url_ = szStatusText;
break;
case BINDSTATUS_MIMETYPEAVAILABLE:
content_type_ = szStatusText;
break;
default:
break;
}
// Track the current progress of the streaming transfer.
stream_size_ = ulProgressMax;
stream_received_ = ulProgress;
return S_OK;
}
HRESULT STDMETHODCALLTYPE StreamOperation::OnStopBinding(HRESULT hresult,
LPCWSTR szError) {
NPReason reason = SUCCEEDED(hresult) ? NPRES_DONE : NPRES_NETWORK_ERR;
USES_CONVERSION;
// Notify the calling plug-in that the transfer has completed.
if (stream_type_ == NP_ASFILE || stream_type_ == NP_ASFILEONLY) {
if (temp_file_) {
fclose(temp_file_);
}
if (reason == NPRES_DONE) {
NPPAsFileArgs arguments = {
owner_->GetNPP(),
GetNPStream(),
W2A(temp_file_name_)
};
CustomSendMessage(m_hWnd, WM_NPP_ASFILE,
reinterpret_cast<LPARAM>(&arguments));
}
}
if (reason == NPRES_DONE) {
NPError error_return;
NPPDestroyStreamArgs destroy_stream_args = {
owner_->GetNPP(),
GetNPStream(),
reason,
&error_return
};
CustomSendMessage(m_hWnd, WM_NPP_DESTROYSTREAM,
reinterpret_cast<LPARAM>(&destroy_stream_args));
ATLASSERT(NPERR_NO_ERROR == error_return);
}
NPPUrlNotifyArgs url_args = {
owner_->GetNPP(),
W2A(url_),
reason,
GetNotifyData()
};
CustomSendMessage(m_hWnd, WM_NPP_URLNOTIFY,
reinterpret_cast<LPARAM>(&url_args));
// Clear the intermediate file from the cache.
_wremove(temp_file_name_);
temp_file_name_ = L"";
// The operation has completed, so tear-down the intermediate window, and
// exit the worker thread.
CustomSendMessage(m_hWnd, WM_TEAR_DOWN, 0);
PostQuitMessage(0);
return S_OK;
}
HRESULT STDMETHODCALLTYPE StreamOperation::GetBindInfo(DWORD *grfBINDF,
BINDINFO *pbindinfo) {
// Request an asynchronous transfer of the data.
*grfBINDF = BINDF_ASYNCHRONOUS | BINDF_ASYNCSTORAGE |
BINDF_GETNEWESTVERSION;
int cbSize = pbindinfo->cbSize;
memset(pbindinfo, 0, cbSize);
pbindinfo->cbSize = cbSize;
pbindinfo->dwBindVerb = BINDVERB_GET;
return S_OK;
}
HRESULT STDMETHODCALLTYPE StreamOperation::OnDataAvailable(
DWORD grfBSCF,
DWORD dwSize,
FORMATETC *pformatetc,
STGMEDIUM *pstgmed) {
if (pstgmed->tymed != TYMED_ISTREAM || !pstgmed->pstm) {
return S_OK;
}
// Don't bother processing any data if the stream has been canceled.
if (cancel_requested_) {
binding_->Abort();
return S_OK;
}
// Notify the plugin that a new stream has been opened.
if (grfBSCF & BSCF_FIRSTDATANOTIFICATION) {
USES_CONVERSION;
np_stream_.url = W2CA(url_);
np_stream_.end = stream_size_;
np_stream_.lastmodified = 0;
np_stream_.notifyData = GetNotifyData();
uint16 stream_type = NP_NORMAL;
NPError np_error;
NPPNewStreamArgs new_stream_args = {
owner_->GetNPP(),
const_cast<char*>(W2CA(GetContentType())),
GetNPStream(),
FALSE,
&stream_type,
&np_error
};
CustomSendMessage(m_hWnd, WM_NPP_NEWSTREAM,
reinterpret_cast<LPARAM>(&new_stream_args));
if (np_error != NPERR_NO_ERROR) {
return E_FAIL;
}
// Cache the stream type requested by the plug-in.
stream_type_ = stream_type;
}
if (grfBSCF & BSCF_INTERMEDIATEDATANOTIFICATION ||
grfBSCF & BSCF_LASTDATANOTIFICATION) {
// Read all of the available data, and pass it to the plug-in, if requested.
HRESULT hr;
char local_data[16384];
DWORD bytes_received_total = 0;
// If a large number of bytes have been received, then this loop can
// take a long time to complete - which will block the user from leaving
// the page as the plug-in waits for all transfers to complete. We
// add a check on cancel_requested_ to allow for early bail-out.
while (bytes_received_total < dwSize &&
!cancel_requested_) {
int bytes_to_read = dwSize - bytes_received_total;
unsigned long bytes_read = 0;
if (bytes_to_read > sizeof(local_data)) {
bytes_to_read = sizeof(local_data);
}
if (stream_type_ == NP_NORMAL || stream_type_ == NP_ASFILE) {
int32 bytes_to_accept;
NPPWriteReadyArgs write_ready_args = {
owner_->GetNPP(),
GetNPStream(),
&bytes_to_accept
};
CustomSendMessage(m_hWnd, WM_NPP_WRITEREADY,
reinterpret_cast<LPARAM>(&write_ready_args));
if (bytes_to_read > bytes_to_accept) {
bytes_to_read = bytes_to_accept;
}
}
// If the plug-in has indicated that it is not prepared to read any data,
// then bail early.
if (bytes_to_read == 0) {
break;
}
hr = pstgmed->pstm->Read(local_data, bytes_to_read, &bytes_read);
if (FAILED(hr) || S_FALSE == hr) {
break;
}
// Pass the data to the plug-in.
if (stream_type_ == NP_NORMAL || stream_type_ == NP_ASFILE) {
int consumed_bytes;
NPPWriteArgs write_args = {
owner_->GetNPP(),
GetNPStream(),
bytes_received_total,
bytes_read,
local_data,
&consumed_bytes
};
CustomSendMessage(m_hWnd, WM_NPP_WRITE,
reinterpret_cast<LPARAM>(&write_args));
ATLASSERT(consumed_bytes == bytes_read);
}
if (stream_type_ == NP_ASFILE || stream_type_ == NP_ASFILEONLY) {
// If the plug-in requested access to the data through a file, then
// create a temporary file and write the data to it.
if (!temp_file_) {
temp_file_name_= _wtempnam(NULL, L"npapi_host_temp");
_wfopen_s(&temp_file_, temp_file_name_, L"wb");
}
fwrite(local_data, bytes_read, 1, temp_file_);
}
bytes_received_total += bytes_read;
}
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE StreamOperation::OnObjectAvailable(REFIID riid,
IUnknown *punk) {
return S_OK;
}
HRESULT StreamOperation::OpenURL(NPPluginProxy *owning_plugin,
const wchar_t *url,
void *notify_data) {
HRESULT hr;
// The StreamOperation instance is created with a ref-count of zero,
// so we explicitly attach a CComPtr to the object to boost the count, and
// manage the lifetime of the object.
CComObject<StreamOperation> *stream_ptr;
CComObject<StreamOperation>::CreateInstance(&stream_ptr);
CComPtr<CComObject<StreamOperation> > stream_object = stream_ptr;
if (!stream_object) {
return E_OUTOFMEMORY;
}
CComPtr<CHostControl> host_control =
owning_plugin->browser_proxy()->GetHostingControl();
CComPtr<IMoniker> base_url_moniker = host_control->GetURLMoniker();
stream_object->SetURL(url);
stream_object->SetNotifyData(notify_data);
stream_object->SetOwner(owning_plugin);
CString full_path;
if (FAILED(hr = ConstructFullURLPath(*stream_object,
base_url_moniker,
&full_path))) {
return hr;
}
stream_object->SetFullURL(full_path);
// Validate the URL. If the URL is invalid there is no need to create a new
// thread only to have it immediately fail.
URL_COMPONENTS components = { sizeof(URL_COMPONENTS) };
if (!InternetCrackUrl(full_path, 0, 0, &components))
return E_INVALIDARG;
if (components.nScheme != INTERNET_SCHEME_FILE &&
components.nScheme != INTERNET_SCHEME_FTP &&
components.nScheme != INTERNET_SCHEME_HTTP &&
components.nScheme != INTERNET_SCHEME_HTTPS) {
return E_INVALIDARG;
}
// Create an object window on this thread that will be sent messages when
// something happens on the worker thread.
HWND temporary_window = stream_object->Create(HWND_DESKTOP);
ATLASSERT(temporary_window);
if (!temporary_window) {
return E_FAIL;
}
// Artificially increment the reference count of the stream_object instance
// to ensure that the object will not be deleted until WM_NC_DESTROY is
// processed and OnFinalMessage is invoked.
// Note: The operator-> is not used, because it returns a type overloading
// the public access of AddRef/Release.
(*stream_object).AddRef();
stream_object->thread_handle_ = reinterpret_cast<HANDLE>(
_beginthreadex(NULL,
0,
WorkerProc,
static_cast<void*>(stream_object),
CREATE_SUSPENDED,
NULL));
ATLASSERT(stream_object->thread_handle_);
if (!stream_object->thread_handle_) {
stream_object->DestroyWindow();
return E_FAIL;
}
owning_plugin->RegisterStreamOperation(stream_object);
if (!ResumeThread(stream_object->thread_handle_)) {
owning_plugin->UnregisterStreamOperation(stream_object);
stream_object->DestroyWindow();
// If the thread never resumed, then we can safely terminate it here - it
// has not had a chance to allocate any resources that would be leaked.
TerminateThread(stream_object->thread_handle_, 0);
return E_FAIL;
}
return S_OK;
}
unsigned int __stdcall StreamOperation::WorkerProc(void* worker_arguments) {
CComObject<StreamOperation> *stream_object =
static_cast<CComObject<StreamOperation> *>(worker_arguments);
ATLASSERT(stream_object);
// Initialize the COM run-time for this new thread.
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
ATLASSERT(SUCCEEDED(hr) && "Failure to initialize worker COM apartment.");
if (FAILED(hr)) {
CustomSendMessage(stream_object->m_hWnd, WM_TEAR_DOWN, 0);
CoUninitialize();
return 0;
}
{
// Get the ActiveX control so the request is within the context of the
// plugin. Among other things, this lets the browser reject file:// uris
// when the page is loaded over http://.
CComPtr<IUnknown> caller;
stream_object->owner_->browser_proxy()->GetHostingControl()->QueryInterface(
IID_IUnknown,
reinterpret_cast<void**>(&caller));
// Note that the OnStopBinding(...) routine, which is always called, will
// post WM_QUIT to this thread.
hr = URLOpenStream(caller, stream_object->GetFullURL(), 0,
static_cast<IBindStatusCallback*>(stream_object));
// Pump messages until WM_QUIT arrives
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
CoUninitialize();
return 0;
}
LRESULT StreamOperation::OnNPPNewStream(UINT uMsg,
WPARAM wParam,
LPARAM lParam,
BOOL& bHandled) {
NPPNewStreamArgs *args = reinterpret_cast<NPPNewStreamArgs*>(lParam);
ATLASSERT(args);
// If the stream was canceled, don't pass the notification to the plug-in.
if (!cancel_requested_) {
*args->return_code_ = owner_->GetPluginFunctions()->newstream(
args->npp_,
args->type_,
args->stream_,
args->seekable_,
args->stype_);
} else {
*args->return_code_ = NPERR_GENERIC_ERROR;
}
if (wParam) {
HANDLE* event_handle = reinterpret_cast<HANDLE*>(wParam);
SetEvent(*event_handle);
}
return 0;
}
LRESULT StreamOperation::OnNPPDestroyStream(UINT uMsg,
WPARAM wParam,
LPARAM lParam,
BOOL& bHandled) {
NPPDestroyStreamArgs *args = reinterpret_cast<NPPDestroyStreamArgs*>(lParam);
ATLASSERT(args);
// If the stream was canceled, don't pass the notification to the plug-in.
if (!cancel_requested_) {
*args->return_code_ = owner_->GetPluginFunctions()->destroystream(
args->npp_,
args->stream_,
args->reason_);
} else {
*args->return_code_ = NPERR_NO_ERROR;
}
if (wParam) {
HANDLE* event_handle = reinterpret_cast<HANDLE*>(wParam);
SetEvent(*event_handle);
}
return 0;
}
LRESULT StreamOperation::OnNPPAsFile(UINT uMsg,
WPARAM wParam,
LPARAM lParam,
BOOL& bHandled) {
NPPAsFileArgs *args = reinterpret_cast<NPPAsFileArgs*>(lParam);
ATLASSERT(args);
// If the stream was canceled, don't pass the notification to the plug-in.
if (!cancel_requested_) {
owner_->GetPluginFunctions()->asfile(args->npp_, args->stream_,
args->fname_);
}
if (wParam) {
HANDLE* event_handle = reinterpret_cast<HANDLE*>(wParam);
SetEvent(*event_handle);
}
return 0;
}
LRESULT StreamOperation::OnNPPUrlNotify(UINT uMsg,
WPARAM wParam,
LPARAM lParam,
BOOL& bHandled) {
NPPUrlNotifyArgs *args = reinterpret_cast<NPPUrlNotifyArgs*>(lParam);
ATLASSERT(args);
// If the stream was canceled, don't pass the notification to the plug-in.
if (!cancel_requested_) {
owner_->GetPluginFunctions()->urlnotify(args->npp_, args->url_,
args->reason_, args->notify_data_);
}
if (wParam) {
HANDLE* event_handle = reinterpret_cast<HANDLE*>(wParam);
SetEvent(*event_handle);
}
return 0;
}
LRESULT StreamOperation::OnNPPWriteReady(UINT uMsg,
WPARAM wParam,
LPARAM lParam,
BOOL& bHandled) {
NPPWriteReadyArgs *args = reinterpret_cast<NPPWriteReadyArgs*>(lParam);
ATLASSERT(args);
// If the stream was canceled, don't pass the notification to the plug-in.
if (!cancel_requested_) {
*args->return_value_ = owner_->GetPluginFunctions()->writeready(
args->npp_,
args->stream_);
} else {
// Indicate to the download thread that 0 bytes are ready to be received.
*args->return_value_ = 0;
}
if (wParam) {
HANDLE* event_handle = reinterpret_cast<HANDLE*>(wParam);
SetEvent(*event_handle);
}
return 0;
}
LRESULT StreamOperation::OnNPPWrite(UINT uMsg,
WPARAM wParam,
LPARAM lParam,
BOOL& bHandled) {
NPPWriteArgs *args = reinterpret_cast<NPPWriteArgs*>(lParam);
ATLASSERT(args);
// If the stream was canceled, don't pass the notification to the plug-in.
if (!cancel_requested_) {
*args->return_value_ = owner_->GetPluginFunctions()->write(
args->npp_,
args->stream_,
args->offset_,
args->len_,
args->buffer_);
} else {
*args->return_value_ = args->len_;
}
if (wParam) {
HANDLE* event_handle = reinterpret_cast<HANDLE*>(wParam);
SetEvent(*event_handle);
}
return 0;
}
LRESULT StreamOperation::OnTearDown(UINT uMsg,
WPARAM wParam,
LPARAM lParam,
BOOL& bHandled) {
// DestroyWindow must be called on the same thread as where the window was
// constructed, so make the call here.
DestroyWindow();
if (wParam) {
HANDLE* event_handle = reinterpret_cast<HANDLE*>(wParam);
SetEvent(*event_handle);
}
return 0;
}
HRESULT StreamOperation::RequestCancellation() {
ATLASSERT(binding_ &&
"Cancellation request on a stream that has not been bound.");
cancel_requested_ = true;
return S_OK;
}