#include "JSAPI2_AsyncDownloader.h" #include "JSAPI2_Security.h" #include "main.h" #include "../Agave/Language/api_language.h" #include "JSAPI.h" #include "../nu/AutoChar.h" #include "../nu/AutoLock.h" #include "api.h" #include "..\Components\wac_network\wac_network_http_receiver_api.h" #include "resource.h" #include "../Plugins/General/gen_ml/ml.h" #include #include "JSAPI2_CallbackManager.h" #define SCRIPT_E_REPORTED 0x80020101 #define SIMULTANEOUS_ASYNCDOWNLOADS 2 std::vector asyncDownloads; Nullsoft::Utility::LockGuard asyncDownloadsLock; bool IsImage(const wchar_t *filename) { FOURCC imgload = svc_imageLoader::getServiceType(); int n = (int) WASABI_API_SVC->service_getNumServices(imgload); for (int i=0; iservice_enumService(imgload,i); if (sf) { svc_imageLoader * l = (svc_imageLoader*)sf->getInterface(); if (l) { if (l->isMine(filename)) { sf->releaseInterface(l); return true; } sf->releaseInterface(l); } } } return false; } bool IsPlaylist(const wchar_t *filename) { if (!AGAVE_API_PLAYLISTMANAGER || !AGAVE_API_PLAYLISTMANAGER->CanLoad(filename)) return false; return true; } bool IsMedia( const wchar_t *filename ) { int a = 0; if ( !in_setmod_noplay( filename, &a ) ) { return false; } return true; } namespace JSAPI2 { class AsyncDownloaderAPICallback : public ifc_downloadManagerCallback { public: AsyncDownloaderAPICallback( const wchar_t *url, const wchar_t *destination_filepath, const wchar_t *onlineServiceId, const wchar_t *onlineServiceName ) { this->hFile = INVALID_HANDLE_VALUE; this->url = _wcsdup( url ); this->destination_filepath = _wcsdup( destination_filepath ); this->onlineServiceId = _wcsdup( onlineServiceId ); if ( onlineServiceName ) this->onlineServiceName = _wcsdup( onlineServiceName ); else this->onlineServiceName = NULL; this->totalSize = 0; this->downloaded = 0; ref_count = 1; } void OnInit(DownloadToken token) { callbackManager.OnInit(this->url, this->onlineServiceId); } void OnConnect(DownloadToken token) { // ---- retrieve total size api_httpreceiver *http = WAC_API_DOWNLOADMANAGER->GetReceiver(token); if (http) { this->totalSize = http->content_length(); } // ---- create file handle hFile = CreateFileW(destination_filepath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if ( hFile == INVALID_HANDLE_VALUE ) { WAC_API_DOWNLOADMANAGER->CancelDownload(token); } callbackManager.OnConnect(this->url, this->onlineServiceId); } void OnData(DownloadToken token, void *data, size_t datalen) { // ---- OnConnect copied here due to dlmgr OnData called first // ---- retrieve total size api_httpreceiver *http = WAC_API_DOWNLOADMANAGER->GetReceiver(token); if ( !this->totalSize && http ) { this->totalSize = http->content_length(); } if ( hFile == INVALID_HANDLE_VALUE ) { // ---- create file handle hFile = CreateFileW(destination_filepath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if ( hFile == INVALID_HANDLE_VALUE ) { WAC_API_DOWNLOADMANAGER->CancelDownload(token); return; } } // ---- OnConnect to be removed once dlmgr is fixed // ---- OnData // ---- if file handle is invalid, then cancel download if ( hFile == INVALID_HANDLE_VALUE ) { WAC_API_DOWNLOADMANAGER->CancelDownload(token); return; } this->downloaded = (size_t)WAC_API_DOWNLOADMANAGER->GetBytesDownloaded(token); if ( datalen > 0 ) { // ---- hFile is valid handle, and write to disk DWORD numWritten = 0; WriteFile(hFile, data, (DWORD)datalen, &numWritten, FALSE); // ---- failed writing the number of datalen characters, cancel download if (numWritten != datalen) { WAC_API_DOWNLOADMANAGER->CancelDownload(token); return; } } // TODO: if killswitch is turned on, then cancel download //if ( downloadStatus.UpdateStatus(p_token, this->downloaded, this->totalSize) ) //{ // WAC_API_DOWNLOADMANAGER->CancelDownload(p_token); //} callbackManager.OnData(url, this->downloaded, this->totalSize, this->onlineServiceId); } void OnCancel( DownloadToken p_token ) { if ( hFile != INVALID_HANDLE_VALUE ) { CloseHandle( hFile ); DeleteFileW( destination_filepath ); } this->resumeNextPendingDownload( p_token ); callbackManager.OnCancel( url, this->onlineServiceId ); this->Release(); } void OnError(DownloadToken p_token, int error) { if ( hFile != INVALID_HANDLE_VALUE ) { CloseHandle(hFile); DeleteFileW(destination_filepath); } this->resumeNextPendingDownload( p_token ); callbackManager.OnError(url, error, this->onlineServiceId); this->Release(); } void OnFinish( DownloadToken p_token ) { if ( hFile != INVALID_HANDLE_VALUE ) { CloseHandle( hFile ); if ( IsMedia( PathFindFileNameW( destination_filepath ) ) ) { LMDB_FILE_ADD_INFOW fi = { const_cast( destination_filepath ), -1, -1 }; sendMlIpc( ML_IPC_DB_ADDORUPDATEFILEW, (WPARAM)&fi ); sendMlIpc( ML_IPC_DB_SYNCDB, 0 ); } } this->resumeNextPendingDownload( p_token ); callbackManager.OnFinish( url, destination_filepath, this->onlineServiceId ); this->Release(); } int GetSource( wchar_t *source, size_t source_cch ) { if ( this->onlineServiceName ) return wcscpy_s( source, source_cch, this->onlineServiceName ); else return 1; } int GetTitle( wchar_t *title, size_t title_cch ) { return wcscpy_s( title, title_cch, PathFindFileNameW( this->destination_filepath ) ); } int GetLocation( wchar_t *location, size_t location_cch ) { return wcscpy_s( location, location_cch, this->destination_filepath ); } size_t AddRef() { return InterlockedIncrement( (LONG *)&ref_count ); } size_t Release() { if ( ref_count == 0 ) return ref_count; LONG r = InterlockedDecrement( (LONG *)&ref_count ); if ( r == 0 ) delete( this ); return r; } private: // private destructor so no one accidentally calls delete directly on this reference counted object ~AsyncDownloaderAPICallback() { if ( url ) free( url ); if ( destination_filepath ) free( destination_filepath ); if ( onlineServiceId ) free( onlineServiceId ); if ( onlineServiceName ) free( onlineServiceName ); } inline void resumeNextPendingDownload( DownloadToken p_token ) { { Nullsoft::Utility::AutoLock lock( asyncDownloadsLock ); size_t l_index = 0; for ( DownloadToken &l_download_token : asyncDownloads ) { if ( l_download_token == p_token ) { asyncDownloads.erase( asyncDownloads.begin() + l_index ); break; } ++l_index; } } for ( DownloadToken &l_download_token : asyncDownloads ) { if ( WAC_API_DOWNLOADMANAGER->IsPending( l_download_token ) ) { WAC_API_DOWNLOADMANAGER->ResumePendingDownload( l_download_token ); break; } } } protected: RECVS_DISPATCH; private: HANDLE hFile; wchar_t *url; wchar_t *destination_filepath; wchar_t *onlineServiceId; wchar_t *onlineServiceName; size_t totalSize; size_t downloaded; LONG ref_count; }; } #define CBCLASS JSAPI2::AsyncDownloaderAPICallback START_DISPATCH; VCB( IFC_DOWNLOADMANAGERCALLBACK_ONFINISH, OnFinish ) VCB( IFC_DOWNLOADMANAGERCALLBACK_ONCANCEL, OnCancel ) VCB( IFC_DOWNLOADMANAGERCALLBACK_ONERROR, OnError ) VCB( IFC_DOWNLOADMANAGERCALLBACK_ONDATA, OnData ) VCB( IFC_DOWNLOADMANAGERCALLBACK_ONCONNECT, OnConnect ) VCB( IFC_DOWNLOADMANAGERCALLBACK_ONINIT, OnInit ) CB( IFC_DOWNLOADMANAGERCALLBACK_GETSOURCE, GetSource ) CB( IFC_DOWNLOADMANAGERCALLBACK_GETTITLE, GetTitle ) CB( IFC_DOWNLOADMANAGERCALLBACK_GETLOCATION, GetLocation ) CB( ADDREF, AddRef ) CB( RELEASE, Release ) END_DISPATCH; #undef CBCLASS JSAPI2::AsyncDownloaderAPI::AsyncDownloaderAPI(const wchar_t *_key, JSAPI::ifc_info *_info) { info = _info; key = _key; refCount = 1; } JSAPI2::AsyncDownloaderAPI::~AsyncDownloaderAPI() { // just in case someone forgot JSAPI2::callbackManager.Deregister(this); size_t index = events.size(); while(index--) { IDispatch *pEvent = events[index]; if (NULL != pEvent) pEvent->Release(); } } #define DISP_TABLE \ CHECK_ID(DownloadMedia)\ CHECK_ID(RegisterForEvents)\ CHECK_ID(UnregisterFromEvents)\ #define CHECK_ID(str) JSAPI_DISP_ENUMIFY(str), enum { DISP_TABLE }; #undef CHECK_ID #define CHECK_ID(str)\ if (CSTR_EQUAL == CompareStringW(lcid, NORM_IGNORECASE, rgszNames[i], -1, L## #str, -1))\ { rgdispid[i] = JSAPI_DISP_ENUMIFY(str); continue; } HRESULT JSAPI2::AsyncDownloaderAPI::GetIDsOfNames(REFIID riid, OLECHAR FAR* FAR* rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgdispid) { bool unknowns = false; for (unsigned int i = 0;i != cNames;i++) { DISP_TABLE; rgdispid[i] = DISPID_UNKNOWN; unknowns = true; } if (unknowns) return DISP_E_UNKNOWNNAME; else return S_OK; } HRESULT JSAPI2::AsyncDownloaderAPI::GetTypeInfo(unsigned int itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo) { return E_NOTIMPL; } HRESULT JSAPI2::AsyncDownloaderAPI::GetTypeInfoCount(unsigned int FAR * pctinfo) { return E_NOTIMPL; } int CALLBACK WINAPI BrowseCallbackProc_Download(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData) { if (uMsg == BFFM_INITIALIZED) { SendMessageW(hwnd, BFFM_SETSELECTIONW, 1, (LPARAM)lpData); // this is not nice but it fixes the selection not working correctly on all OSes EnumChildWindows(hwnd, browseEnumProc, 0); } if (uMsg == WM_CREATE) SetWindowTextW(hwnd,getStringW(IDS_SELDOWNLOADDIR,NULL,0)); return 0; } void GetPathToStore(wchar_t path_to_store[MAX_PATH]); bool GetOnlineDownloadPath(const wchar_t *key, const wchar_t *svcname, wchar_t path_to_store[MAX_PATH]) { //retrieve online service specific download path GetPrivateProfileStringW(key,L"downloadpath",NULL,path_to_store,MAX_PATH,JSAPI2_INIFILE); //if found then return, otherwise allow user to specify if (path_to_store && path_to_store[0]) return true; //default music folder GetPathToStore(path_to_store); //popup dialog to allow user select and specify online service download path BROWSEINFOW bi={0}; wchar_t name[MAX_PATH] = {0}; wchar_t title[256] = {0}; bi.hwndOwner = g_dialog_box_parent?g_dialog_box_parent:hMainWindow; bi.pszDisplayName = name; StringCchPrintfW(title,256,getStringW(IDS_ONLINESERVICE_SELDOWNLOADDIR, 0, 0),svcname); bi.lpszTitle = title; bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE; bi.lpfn = BrowseCallbackProc_Download; bi.lParam = (LPARAM)path_to_store; ITEMIDLIST *idlist = SHBrowseForFolderW(&bi); if (idlist) { SHGetPathFromIDListW(idlist, path_to_store); Shell_Free(idlist); WritePrivateProfileStringW(key,L"downloadpath",path_to_store,JSAPI2_INIFILE); return true; } return false; } void CleanNameForPath(wchar_t *name) { while (name && *name) { switch(*name) { case L'?': case L'*': case L'|': *name = L'_'; break; case '/': case L'\\': case L':': *name = L'-'; break; case L'\"': *name = L'\''; break; case L'<': *name = L'('; break; case L'>': *name = L')'; break; } name++; } } HRESULT JSAPI2::AsyncDownloaderAPI::DownloadMedia(WORD wFlags, DISPPARAMS FAR *pdispparams, VARIANT FAR *pvarResult, unsigned int FAR *puArgErr) { JSAPI_VERIFY_METHOD(wFlags); JSAPI_VERIFY_PARAMCOUNT_OPTIONAL(pdispparams, 1, 3); JSAPI_VERIFY_PARAMTYPE(pdispparams, 1, VT_BSTR, puArgErr); //url JSAPI_VERIFY_PARAMTYPE_OPTIONAL(pdispparams, 2, VT_BSTR, puArgErr); //destination file JSAPI_VERIFY_PARAMTYPE_OPTIONAL(pdispparams, 3, VT_BOOL, puArgErr); //add to media library or not JSAPI_INIT_RESULT(pvarResult, VT_BOOL); if (security.GetActionAuthorization(L"downloader", L"downloadmedia", key, info, JSAPI2::api_security::ACTION_PROMPT) == JSAPI2::api_security::ACTION_ALLOWED) { JSAPI_SET_RESULT(pvarResult, boolVal, VARIANT_TRUE); const wchar_t *url = JSAPI_PARAM(pdispparams, 1).bstrVal; wchar_t *destFileSpec=JSAPI_PARAM_OPTIONAL(pdispparams, 2, bstrVal, PathFindFileNameW(url)); //filter reserved characters in file name CleanNameForPath(destFileSpec); // verify that passed-in URL is a valid media type if (!url || !destFileSpec || (!IsImage(destFileSpec) && !IsPlaylist(destFileSpec) && !IsMedia(destFileSpec))) { JSAPI_SET_RESULT(pvarResult, boolVal, VARIANT_FALSE); return S_OK; } wchar_t path_to_store[MAX_PATH] = {0}; if (GetOnlineDownloadPath(this->key, this->info->GetName(), path_to_store)) { CreateDirectoryW(path_to_store, NULL); wchar_t destfile[MAX_PATH] = {0}; PathCombineW(destfile, path_to_store, destFileSpec); JSAPI2::AsyncDownloaderAPICallback *callback = new JSAPI2::AsyncDownloaderAPICallback(url, destfile, key, this->info->GetName()); { Nullsoft::Utility::AutoLock lock(asyncDownloadsLock); if (asyncDownloads.size() < SIMULTANEOUS_ASYNCDOWNLOADS) { DownloadToken dt = WAC_API_DOWNLOADMANAGER->DownloadEx(AutoChar(url), callback, api_downloadManager::DOWNLOADEX_CALLBACK | api_downloadManager::DOWNLOADEX_UI); asyncDownloads.push_back(dt); } else { DownloadToken dt = WAC_API_DOWNLOADMANAGER->DownloadEx(AutoChar(url), callback, api_downloadManager::DOWNLOADEX_CALLBACK | api_downloadManager::DOWNLOADEX_PENDING | api_downloadManager::DOWNLOADEX_UI); asyncDownloads.push_back(dt); } } } else { JSAPI_SET_RESULT(pvarResult, boolVal, VARIANT_FALSE); } } else { JSAPI_SET_RESULT(pvarResult, boolVal, VARIANT_FALSE); } return S_OK; } HRESULT JSAPI2::AsyncDownloaderAPI::RegisterForEvents(WORD wFlags, DISPPARAMS FAR *pdispparams, VARIANT FAR *pvarResult, unsigned int FAR *puArgErr) { JSAPI_VERIFY_METHOD(wFlags); JSAPI_VERIFY_PARAMCOUNT(pdispparams, 1); JSAPI_VERIFY_PARAMTYPE(pdispparams, 1, VT_DISPATCH, puArgErr); JSAPI_INIT_RESULT(pvarResult, VT_BOOL); switch (security.GetActionAuthorization(L"downloader", L"events", key, info, JSAPI2::api_security::ACTION_PROMPT)) { case JSAPI2::api_security::ACTION_DISALLOWED: JSAPI_SET_RESULT(pvarResult, boolVal, VARIANT_FALSE); break; case JSAPI2::api_security::ACTION_ALLOWED: { /** if this is the first time someone is registering an event ** add ourselves to the callback manager */ if (events.empty()) JSAPI2::callbackManager.Register(this); IDispatch *event = JSAPI_PARAM(pdispparams, 1).pdispVal; event->AddRef(); // TODO: benski> not sure, but we might need to: event->AddRef(); events.push_back(event); JSAPI_SET_RESULT(pvarResult, boolVal, VARIANT_TRUE); } break; } return S_OK; } HRESULT JSAPI2::AsyncDownloaderAPI::UnregisterFromEvents(WORD wFlags, DISPPARAMS FAR *pdispparams, VARIANT FAR *pvarResult, unsigned int FAR *puArgErr) { JSAPI_VERIFY_METHOD(wFlags); JSAPI_VERIFY_PARAMCOUNT(pdispparams, 1); JSAPI_VERIFY_PARAMTYPE(pdispparams, 1, VT_DISPATCH, puArgErr); IDispatch *event = JSAPI_PARAM(pdispparams, 1).pdispVal; // TODO: benski> not sure, but we might need to: event->Release(); size_t index = events.size(); while(index--) { if (events[index] == event) { events.erase(events.begin() + index); event->Release(); } } /** if we don't have any more event listeners ** remove ourselves from the callback manager */ if (events.empty()) JSAPI2::callbackManager.Deregister(this); return S_OK; } #undef CHECK_ID #define CHECK_ID(str) case JSAPI_DISP_ENUMIFY(str): return str(wFlags, pdispparams, pvarResult, puArgErr); HRESULT JSAPI2::AsyncDownloaderAPI::Invoke(DISPID dispid, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR *pdispparams, VARIANT FAR *pvarResult, EXCEPINFO FAR * pexecinfo, unsigned int FAR *puArgErr) { switch (dispid) { DISP_TABLE } return DISP_E_MEMBERNOTFOUND; } STDMETHODIMP JSAPI2::AsyncDownloaderAPI::QueryInterface(REFIID riid, PVOID *ppvObject) { if (!ppvObject) return E_POINTER; else if (IsEqualIID(riid, IID_IDispatch)) *ppvObject = (IDispatch *)this; else if (IsEqualIID(riid, IID_IUnknown)) *ppvObject = this; else { *ppvObject = NULL; return E_NOINTERFACE; } AddRef(); return S_OK; } ULONG JSAPI2::AsyncDownloaderAPI::AddRef(void) { return InterlockedIncrement(&refCount); } ULONG JSAPI2::AsyncDownloaderAPI::Release(void) { LONG lRef = InterlockedDecrement(&refCount); if (lRef == 0) delete this; return lRef; } void JSAPI2::AsyncDownloaderAPI::InvokeEvent(const wchar_t *eventName, JSAPI::CallbackParameters::PropertyTemplate *parameters, size_t parametersCount) { size_t index = events.size(); if (0 == index) { JSAPI2::callbackManager.Deregister(this); return; } JSAPI::CallbackParameters *eventData= new JSAPI::CallbackParameters; if (NULL == eventData) return; eventData->AddString(L"event", eventName); if (NULL != parameters && 0 != parametersCount) eventData->AddPropertyIndirect(parameters, parametersCount); HRESULT hr; while (index--) { IDispatch *pEvent = events[index]; if (NULL != pEvent) { hr = JSAPI::InvokeEvent(eventData, pEvent); if (FAILED(hr) && SCRIPT_E_REPORTED != hr) { events.erase(events.begin() + index); pEvent->Release(); } } } if (events.empty()) JSAPI2::callbackManager.Deregister(this); eventData->Release(); } void JSAPI2::AsyncDownloaderAPI::OnInit(const wchar_t *url) { JSAPI::CallbackParameters::PropertyTemplate parameter = {JSAPI::CallbackParameters::typeString, L"url", (ULONG_PTR)url}; InvokeEvent(L"OnInit", ¶meter, 1); } void JSAPI2::AsyncDownloaderAPI::OnConnect(const wchar_t *url) { JSAPI::CallbackParameters::PropertyTemplate parameter = {JSAPI::CallbackParameters::typeString, L"url", (ULONG_PTR)url}; InvokeEvent(L"OnConnect", ¶meter, 1); } void JSAPI2::AsyncDownloaderAPI::OnData(const wchar_t *url, size_t downloadedlen, size_t totallen) { JSAPI::CallbackParameters::PropertyTemplate parameter[3] = {{JSAPI::CallbackParameters::typeString, L"url", (ULONG_PTR)url}, {JSAPI::CallbackParameters::typeLong, L"downloadedlen", (ULONG_PTR)downloadedlen}, {JSAPI::CallbackParameters::typeLong, L"totallen", (ULONG_PTR)totallen}}; InvokeEvent(L"OnData", ¶meter[0], 3); } void JSAPI2::AsyncDownloaderAPI::OnCancel(const wchar_t *url) { JSAPI::CallbackParameters::PropertyTemplate parameter = {JSAPI::CallbackParameters::typeString, L"url", (ULONG_PTR)url}; InvokeEvent(L"OnCancel", ¶meter, 1); } void JSAPI2::AsyncDownloaderAPI::OnError(const wchar_t *url, int error) { JSAPI::CallbackParameters::PropertyTemplate parameter[2] = {{JSAPI::CallbackParameters::typeString, L"url", (ULONG_PTR)url}, {JSAPI::CallbackParameters::typeLong, L"error", (ULONG_PTR)error}}; InvokeEvent(L"OnError", ¶meter[0], 2); } void JSAPI2::AsyncDownloaderAPI::OnFinish(const wchar_t *url, const wchar_t *destfilename) { JSAPI::CallbackParameters::PropertyTemplate parameter[2] = {{JSAPI::CallbackParameters::typeString, L"url", (ULONG_PTR)url}, {JSAPI::CallbackParameters::typeString, L"destfilename", (ULONG_PTR)destfilename}}; InvokeEvent(L"OnFinish", ¶meter[0], 2); } const wchar_t *JSAPI2::AsyncDownloaderAPI::GetKey() { return this->key; }