diff options
Diffstat (limited to 'widget/windows/nsFilePicker.cpp')
-rw-r--r-- | widget/windows/nsFilePicker.cpp | 1383 |
1 files changed, 1383 insertions, 0 deletions
diff --git a/widget/windows/nsFilePicker.cpp b/widget/windows/nsFilePicker.cpp new file mode 100644 index 000000000..53857cf5e --- /dev/null +++ b/widget/windows/nsFilePicker.cpp @@ -0,0 +1,1383 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsFilePicker.h" + +#include <shlobj.h> +#include <shlwapi.h> +#include <cderr.h> + +#include "mozilla/mscom/EnsureMTA.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WindowsVersion.h" +#include "nsReadableUtils.h" +#include "nsNetUtil.h" +#include "nsWindow.h" +#include "nsILoadContext.h" +#include "nsIServiceManager.h" +#include "nsIURL.h" +#include "nsIStringBundle.h" +#include "nsEnumeratorUtils.h" +#include "nsCRT.h" +#include "nsString.h" +#include "nsToolkit.h" +#include "WinUtils.h" +#include "nsPIDOMWindow.h" +#include "GeckoProfiler.h" + +using mozilla::IsVistaOrLater; +using mozilla::IsWin8OrLater; +using mozilla::MakeUnique; +using mozilla::mscom::EnsureMTA; +using mozilla::UniquePtr; +using namespace mozilla::widget; + +char16_t *nsFilePicker::mLastUsedUnicodeDirectory; +char nsFilePicker::mLastUsedDirectory[MAX_PATH+1] = { 0 }; + +static const wchar_t kDialogPtrProp[] = L"DialogPtrProperty"; +static const DWORD kDialogTimerID = 9999; +static const unsigned long kDialogTimerTimeout = 300; + +#define MAX_EXTENSION_LENGTH 10 +#define FILE_BUFFER_SIZE 4096 + +typedef DWORD FILEOPENDIALOGOPTIONS; + +/////////////////////////////////////////////////////////////////////////////// +// Helper classes + +// Manages matching SuppressBlurEvents calls on the parent widget. +class AutoSuppressEvents +{ +public: + explicit AutoSuppressEvents(nsIWidget* aWidget) : + mWindow(static_cast<nsWindow *>(aWidget)) { + SuppressWidgetEvents(true); + } + + ~AutoSuppressEvents() { + SuppressWidgetEvents(false); + } +private: + void SuppressWidgetEvents(bool aFlag) { + if (mWindow) { + mWindow->SuppressBlurEvents(aFlag); + } + } + RefPtr<nsWindow> mWindow; +}; + +// Manages the current working path. +class AutoRestoreWorkingPath +{ +public: + AutoRestoreWorkingPath() { + DWORD bufferLength = GetCurrentDirectoryW(0, nullptr); + mWorkingPath = MakeUnique<wchar_t[]>(bufferLength); + if (GetCurrentDirectoryW(bufferLength, mWorkingPath.get()) == 0) { + mWorkingPath = nullptr; + } + } + + ~AutoRestoreWorkingPath() { + if (HasWorkingPath()) { + ::SetCurrentDirectoryW(mWorkingPath.get()); + } + } + + inline bool HasWorkingPath() const { + return mWorkingPath != nullptr; + } +private: + UniquePtr<wchar_t[]> mWorkingPath; +}; + +// Manages NS_NATIVE_TMP_WINDOW child windows. NS_NATIVE_TMP_WINDOWs are +// temporary child windows of mParentWidget created to address RTL issues +// in picker dialogs. We are responsible for destroying these. +class AutoDestroyTmpWindow +{ +public: + explicit AutoDestroyTmpWindow(HWND aTmpWnd) : + mWnd(aTmpWnd) { + } + + ~AutoDestroyTmpWindow() { + if (mWnd) + DestroyWindow(mWnd); + } + + inline HWND get() const { return mWnd; } +private: + HWND mWnd; +}; + +// Manages matching PickerOpen/PickerClosed calls on the parent widget. +class AutoWidgetPickerState +{ +public: + explicit AutoWidgetPickerState(nsIWidget* aWidget) : + mWindow(static_cast<nsWindow *>(aWidget)) { + PickerState(true); + } + + ~AutoWidgetPickerState() { + PickerState(false); + } +private: + void PickerState(bool aFlag) { + if (mWindow) { + if (aFlag) + mWindow->PickerOpen(); + else + mWindow->PickerClosed(); + } + } + RefPtr<nsWindow> mWindow; +}; + +// Manages a simple callback timer +class AutoTimerCallbackCancel +{ +public: + AutoTimerCallbackCancel(nsFilePicker* aTarget, + nsTimerCallbackFunc aCallbackFunc) { + Init(aTarget, aCallbackFunc); + } + + ~AutoTimerCallbackCancel() { + if (mPickerCallbackTimer) { + mPickerCallbackTimer->Cancel(); + } + } + +private: + void Init(nsFilePicker* aTarget, + nsTimerCallbackFunc aCallbackFunc) { + mPickerCallbackTimer = do_CreateInstance("@mozilla.org/timer;1"); + if (!mPickerCallbackTimer) { + NS_WARNING("do_CreateInstance for timer failed??"); + return; + } + mPickerCallbackTimer->InitWithFuncCallback(aCallbackFunc, + aTarget, + kDialogTimerTimeout, + nsITimer::TYPE_REPEATING_SLACK); + } + nsCOMPtr<nsITimer> mPickerCallbackTimer; + +}; + +/////////////////////////////////////////////////////////////////////////////// +// nsIFilePicker + +nsFilePicker::nsFilePicker() : + mSelectedType(1) + , mDlgWnd(nullptr) + , mFDECookie(0) +{ + CoInitialize(nullptr); +} + +nsFilePicker::~nsFilePicker() +{ + if (mLastUsedUnicodeDirectory) { + free(mLastUsedUnicodeDirectory); + mLastUsedUnicodeDirectory = nullptr; + } + CoUninitialize(); +} + +NS_IMPL_ISUPPORTS(nsFilePicker, nsIFilePicker) + +NS_IMETHODIMP nsFilePicker::Init(mozIDOMWindowProxy *aParent, const nsAString& aTitle, int16_t aMode) +{ + nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryInterface(aParent); + nsIDocShell* docShell = window ? window->GetDocShell() : nullptr; + mLoadContext = do_QueryInterface(docShell); + + return nsBaseFilePicker::Init(aParent, aTitle, aMode); +} + +STDMETHODIMP nsFilePicker::QueryInterface(REFIID refiid, void** ppvResult) +{ + *ppvResult = nullptr; + if (IID_IUnknown == refiid || + refiid == IID_IFileDialogEvents) { + *ppvResult = this; + } + + if (nullptr != *ppvResult) { + ((LPUNKNOWN)*ppvResult)->AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +/* + * XP picker callbacks + */ + +// Show - Display the file dialog +int CALLBACK +BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData) +{ + if (uMsg == BFFM_INITIALIZED) + { + char16_t * filePath = (char16_t *) lpData; + if (filePath) + ::SendMessageW(hwnd, BFFM_SETSELECTIONW, + TRUE /* true because lpData is a path string */, + lpData); + } + return 0; +} + +static void +EnsureWindowVisible(HWND hwnd) +{ + // Obtain the monitor which has the largest area of intersection + // with the window, or nullptr if there is no intersection. + HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONULL); + if (!monitor) { + // The window is not visible, we should reposition it to the same place as its parent + HWND parentHwnd = GetParent(hwnd); + RECT parentRect; + GetWindowRect(parentHwnd, &parentRect); + SetWindowPos(hwnd, nullptr, parentRect.left, parentRect.top, 0, 0, + SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER); + } +} + +// Callback hook which will ensure that the window is visible. Currently +// only in use on os <= XP. +UINT_PTR CALLBACK +nsFilePicker::FilePickerHook(HWND hwnd, + UINT msg, + WPARAM wParam, + LPARAM lParam) +{ + switch(msg) { + case WM_NOTIFY: + { + LPOFNOTIFYW lpofn = (LPOFNOTIFYW) lParam; + if (!lpofn || !lpofn->lpOFN) { + return 0; + } + + if (CDN_INITDONE == lpofn->hdr.code) { + // The Window will be automatically moved to the last position after + // CDN_INITDONE. We post a message to ensure the window will be visible + // so it will be done after the automatic last position window move. + PostMessage(hwnd, MOZ_WM_ENSUREVISIBLE, 0, 0); + } + } + break; + case MOZ_WM_ENSUREVISIBLE: + EnsureWindowVisible(GetParent(hwnd)); + break; + case WM_INITDIALOG: + { + OPENFILENAMEW* pofn = reinterpret_cast<OPENFILENAMEW*>(lParam); + SetProp(hwnd, kDialogPtrProp, (HANDLE)pofn->lCustData); + nsFilePicker* picker = reinterpret_cast<nsFilePicker*>(pofn->lCustData); + if (picker) { + picker->SetDialogHandle(hwnd); + SetTimer(hwnd, kDialogTimerID, kDialogTimerTimeout, nullptr); + } + } + break; + case WM_TIMER: + { + // Check to see if our parent has been torn down, if so, we close too. + if (wParam == kDialogTimerID) { + nsFilePicker* picker = + reinterpret_cast<nsFilePicker*>(GetProp(hwnd, kDialogPtrProp)); + if (picker && picker->ClosePickerIfNeeded(true)) { + KillTimer(hwnd, kDialogTimerID); + } + } + } + break; + } + return 0; +} + + +// Callback hook which will dynamically allocate a buffer large enough +// for the file picker dialog. Currently only in use on os <= XP. +UINT_PTR CALLBACK +nsFilePicker::MultiFilePickerHook(HWND hwnd, + UINT msg, + WPARAM wParam, + LPARAM lParam) +{ + switch (msg) { + case WM_INITDIALOG: + { + // Finds the child drop down of a File Picker dialog and sets the + // maximum amount of text it can hold when typed in manually. + // A wParam of 0 mean 0x7FFFFFFE characters. + HWND comboBox = FindWindowEx(GetParent(hwnd), nullptr, + L"ComboBoxEx32", nullptr ); + if(comboBox) + SendMessage(comboBox, CB_LIMITTEXT, 0, 0); + // Store our nsFilePicker ptr for future use + OPENFILENAMEW* pofn = reinterpret_cast<OPENFILENAMEW*>(lParam); + SetProp(hwnd, kDialogPtrProp, (HANDLE)pofn->lCustData); + nsFilePicker* picker = + reinterpret_cast<nsFilePicker*>(pofn->lCustData); + if (picker) { + picker->SetDialogHandle(hwnd); + SetTimer(hwnd, kDialogTimerID, kDialogTimerTimeout, nullptr); + } + } + break; + case WM_NOTIFY: + { + LPOFNOTIFYW lpofn = (LPOFNOTIFYW) lParam; + if (!lpofn || !lpofn->lpOFN) { + return 0; + } + // CDN_SELCHANGE is sent when the selection in the list box of the file + // selection dialog changes + if (lpofn->hdr.code == CDN_SELCHANGE) { + HWND parentHWND = GetParent(hwnd); + + // Get the required size for the selected files buffer + UINT newBufLength = 0; + int requiredBufLength = CommDlg_OpenSave_GetSpecW(parentHWND, + nullptr, 0); + if(requiredBufLength >= 0) + newBufLength += requiredBufLength; + else + newBufLength += MAX_PATH; + + // If the user selects multiple files, the buffer contains the + // current directory followed by the file names of the selected + // files. So make room for the directory path. If the user + // selects a single file, it is no harm to add extra space. + requiredBufLength = CommDlg_OpenSave_GetFolderPathW(parentHWND, + nullptr, 0); + if(requiredBufLength >= 0) + newBufLength += requiredBufLength; + else + newBufLength += MAX_PATH; + + // Check if lpstrFile and nMaxFile are large enough + if (newBufLength > lpofn->lpOFN->nMaxFile) { + if (lpofn->lpOFN->lpstrFile) + delete[] lpofn->lpOFN->lpstrFile; + + // We allocate FILE_BUFFER_SIZE more bytes than is needed so that + // if the user selects a file and holds down shift and down to + // select additional items, we will not continuously reallocate + newBufLength += FILE_BUFFER_SIZE; + + wchar_t* filesBuffer = new wchar_t[newBufLength]; + ZeroMemory(filesBuffer, newBufLength * sizeof(wchar_t)); + + lpofn->lpOFN->lpstrFile = filesBuffer; + lpofn->lpOFN->nMaxFile = newBufLength; + } + } + } + break; + case WM_TIMER: + { + // Check to see if our parent has been torn down, if so, we close too. + if (wParam == kDialogTimerID) { + nsFilePicker* picker = + reinterpret_cast<nsFilePicker*>(GetProp(hwnd, kDialogPtrProp)); + if (picker && picker->ClosePickerIfNeeded(true)) { + KillTimer(hwnd, kDialogTimerID); + } + } + } + break; + } + + return FilePickerHook(hwnd, msg, wParam, lParam); +} + +/* + * Vista+ callbacks + */ + +HRESULT +nsFilePicker::OnFileOk(IFileDialog *pfd) +{ + return S_OK; +} + +HRESULT +nsFilePicker::OnFolderChanging(IFileDialog *pfd, + IShellItem *psiFolder) +{ + return S_OK; +} + +HRESULT +nsFilePicker::OnFolderChange(IFileDialog *pfd) +{ + return S_OK; +} + +HRESULT +nsFilePicker::OnSelectionChange(IFileDialog *pfd) +{ + return S_OK; +} + +HRESULT +nsFilePicker::OnShareViolation(IFileDialog *pfd, + IShellItem *psi, + FDE_SHAREVIOLATION_RESPONSE *pResponse) +{ + return S_OK; +} + +HRESULT +nsFilePicker::OnTypeChange(IFileDialog *pfd) +{ + // Failures here result in errors due to security concerns. + RefPtr<IOleWindow> win; + pfd->QueryInterface(IID_IOleWindow, getter_AddRefs(win)); + if (!win) { + NS_ERROR("Could not retrieve the IOleWindow interface for IFileDialog."); + return S_OK; + } + HWND hwnd = nullptr; + win->GetWindow(&hwnd); + if (!hwnd) { + NS_ERROR("Could not retrieve the HWND for IFileDialog."); + return S_OK; + } + + SetDialogHandle(hwnd); + return S_OK; +} + +HRESULT +nsFilePicker::OnOverwrite(IFileDialog *pfd, + IShellItem *psi, + FDE_OVERWRITE_RESPONSE *pResponse) +{ + return S_OK; +} + +/* + * Close on parent close logic + */ + +bool +nsFilePicker::ClosePickerIfNeeded(bool aIsXPDialog) +{ + if (!mParentWidget || !mDlgWnd) + return false; + + nsWindow *win = static_cast<nsWindow *>(mParentWidget.get()); + // Note, the xp callbacks hand us an inner window, so we have to step up + // one to get the actual dialog. + HWND dlgWnd; + if (aIsXPDialog) + dlgWnd = GetParent(mDlgWnd); + else + dlgWnd = mDlgWnd; + if (IsWindow(dlgWnd) && IsWindowVisible(dlgWnd) && win->DestroyCalled()) { + wchar_t className[64]; + // Make sure we have the right window + if (GetClassNameW(dlgWnd, className, mozilla::ArrayLength(className)) && + !wcscmp(className, L"#32770") && + DestroyWindow(dlgWnd)) { + mDlgWnd = nullptr; + return true; + } + } + return false; +} + +void +nsFilePicker::PickerCallbackTimerFunc(nsITimer *aTimer, void *aCtx) +{ + nsFilePicker* picker = (nsFilePicker*)aCtx; + if (picker->ClosePickerIfNeeded(false)) { + aTimer->Cancel(); + } +} + +void +nsFilePicker::SetDialogHandle(HWND aWnd) +{ + if (!aWnd || mDlgWnd) + return; + mDlgWnd = aWnd; +} + +/* + * Folder picker invocation + */ + +// Open the older XP style folder picker dialog. We end up in this call +// on XP systems or when platform is built without the longhorn SDK. +bool +nsFilePicker::ShowXPFolderPicker(const nsString& aInitialDir) +{ + bool result = false; + + auto dirBuffer = MakeUnique<wchar_t[]>(FILE_BUFFER_SIZE); + wcsncpy(dirBuffer.get(), aInitialDir.get(), FILE_BUFFER_SIZE); + dirBuffer[FILE_BUFFER_SIZE-1] = '\0'; + + AutoDestroyTmpWindow adtw((HWND)(mParentWidget.get() ? + mParentWidget->GetNativeData(NS_NATIVE_TMP_WINDOW) : nullptr)); + + BROWSEINFOW browserInfo = {0}; + browserInfo.pidlRoot = nullptr; + browserInfo.pszDisplayName = dirBuffer.get(); + browserInfo.lpszTitle = mTitle.get(); + browserInfo.ulFlags = BIF_USENEWUI | BIF_RETURNONLYFSDIRS; + browserInfo.hwndOwner = adtw.get(); + browserInfo.iImage = 0; + browserInfo.lParam = reinterpret_cast<LPARAM>(this); + + if (!aInitialDir.IsEmpty()) { + // the dialog is modal so that |initialDir.get()| will be valid in + // BrowserCallbackProc. Thus, we don't need to clone it. + browserInfo.lParam = (LPARAM) aInitialDir.get(); + browserInfo.lpfn = &BrowseCallbackProc; + } else { + browserInfo.lParam = 0; + browserInfo.lpfn = nullptr; + } + + LPITEMIDLIST list = ::SHBrowseForFolderW(&browserInfo); + if (list) { + result = ::SHGetPathFromIDListW(list, dirBuffer.get()); + if (result) + mUnicodeFile.Assign(static_cast<const wchar_t*>(dirBuffer.get())); + // free PIDL + CoTaskMemFree(list); + } + + return result; +} + +/* + * Show a folder picker post Windows XP + * + * @param aInitialDir The initial directory, the last used directory will be + * used if left blank. + * @param aWasInitError Out parameter will hold true if there was an error + * before the folder picker is shown. + * @return true if a file was selected successfully. +*/ +bool +nsFilePicker::ShowFolderPicker(const nsString& aInitialDir, bool &aWasInitError) +{ + if (!IsWin8OrLater()) { + // Some Windows 7 users are experiencing a race condition when some dlls + // that are loaded by the file picker cause a crash while attempting to shut + // down the COM multithreaded apartment. By instantiating EnsureMTA, we hold + // an additional reference to the MTA that should prevent this race, since + // the MTA will remain alive until shutdown. + EnsureMTA ensureMTA; + } + + RefPtr<IFileOpenDialog> dialog; + if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC, + IID_IFileOpenDialog, + getter_AddRefs(dialog)))) { + aWasInitError = true; + return false; + } + aWasInitError = false; + + // hook up event callbacks + dialog->Advise(this, &mFDECookie); + + // options + FILEOPENDIALOGOPTIONS fos = FOS_PICKFOLDERS; + dialog->SetOptions(fos); + + // initial strings + dialog->SetTitle(mTitle.get()); + + if (!mOkButtonLabel.IsEmpty()) { + dialog->SetOkButtonLabel(mOkButtonLabel.get()); + } + + if (!aInitialDir.IsEmpty()) { + RefPtr<IShellItem> folder; + if (SUCCEEDED( + WinUtils::SHCreateItemFromParsingName(aInitialDir.get(), nullptr, + IID_IShellItem, + getter_AddRefs(folder)))) { + dialog->SetFolder(folder); + } + } + + AutoDestroyTmpWindow adtw((HWND)(mParentWidget.get() ? + mParentWidget->GetNativeData(NS_NATIVE_TMP_WINDOW) : nullptr)); + + // display + RefPtr<IShellItem> item; + if (FAILED(dialog->Show(adtw.get())) || + FAILED(dialog->GetResult(getter_AddRefs(item))) || + !item) { + dialog->Unadvise(mFDECookie); + return false; + } + dialog->Unadvise(mFDECookie); + + // results + + // If the user chose a Win7 Library, resolve to the library's + // default save folder. + RefPtr<IShellItem> folderPath; + RefPtr<IShellLibrary> shellLib; + CoCreateInstance(CLSID_ShellLibrary, nullptr, CLSCTX_INPROC, + IID_IShellLibrary, getter_AddRefs(shellLib)); + if (shellLib && + SUCCEEDED(shellLib->LoadLibraryFromItem(item, STGM_READ)) && + SUCCEEDED(shellLib->GetDefaultSaveFolder(DSFT_DETECT, IID_IShellItem, + getter_AddRefs(folderPath)))) { + item.swap(folderPath); + } + + // get the folder's file system path + return WinUtils::GetShellItemPath(item, mUnicodeFile); +} + +/* + * File open and save picker invocation + */ + +/* static */ bool +nsFilePicker::GetFileNameWrapper(OPENFILENAMEW* ofn, PickerType aType) +{ + MOZ_SEH_TRY { + if (aType == PICKER_TYPE_OPEN) + return ::GetOpenFileNameW(ofn); + else if (aType == PICKER_TYPE_SAVE) + return ::GetSaveFileNameW(ofn); + } MOZ_SEH_EXCEPT(true) { + NS_ERROR("nsFilePicker GetFileName win32 call generated an exception! This is bad!"); + } + return false; +} + +bool +nsFilePicker::FilePickerWrapper(OPENFILENAMEW* ofn, PickerType aType) +{ + if (!ofn) + return false; + AutoWidgetPickerState awps(mParentWidget); + return GetFileNameWrapper(ofn, aType); +} + +bool +nsFilePicker::ShowXPFilePicker(const nsString& aInitialDir) +{ + OPENFILENAMEW ofn = {0}; + ofn.lStructSize = sizeof(ofn); + nsString filterBuffer = mFilterList; + + auto fileBuffer = MakeUnique<wchar_t[]>(FILE_BUFFER_SIZE); + wcsncpy(fileBuffer.get(), mDefaultFilePath.get(), FILE_BUFFER_SIZE); + fileBuffer[FILE_BUFFER_SIZE-1] = '\0'; // null terminate in case copy truncated + + if (!aInitialDir.IsEmpty()) { + ofn.lpstrInitialDir = aInitialDir.get(); + } + + AutoDestroyTmpWindow adtw((HWND) (mParentWidget.get() ? + mParentWidget->GetNativeData(NS_NATIVE_TMP_WINDOW) : nullptr)); + + ofn.lpstrTitle = (LPCWSTR)mTitle.get(); + ofn.lpstrFilter = (LPCWSTR)filterBuffer.get(); + ofn.nFilterIndex = mSelectedType; + ofn.lpstrFile = fileBuffer.get(); + ofn.nMaxFile = FILE_BUFFER_SIZE; + ofn.hwndOwner = adtw.get(); + ofn.lCustData = reinterpret_cast<LPARAM>(this); + ofn.Flags = OFN_SHAREAWARE | OFN_LONGNAMES | OFN_OVERWRITEPROMPT | + OFN_HIDEREADONLY | OFN_PATHMUSTEXIST | OFN_ENABLESIZING | + OFN_EXPLORER; + + // Windows Vista and up won't allow you to use the new looking dialogs with + // a hook procedure. The hook procedure fixes a problem on XP dialogs for + // file picker visibility. Vista and up automatically ensures the file + // picker is always visible. + if (!IsVistaOrLater()) { + ofn.lpfnHook = FilePickerHook; + ofn.Flags |= OFN_ENABLEHOOK; + } + + // Handle add to recent docs settings + if (IsPrivacyModeEnabled() || !mAddToRecentDocs) { + ofn.Flags |= OFN_DONTADDTORECENT; + } + + NS_NAMED_LITERAL_STRING(htmExt, "html"); + + if (!mDefaultExtension.IsEmpty()) { + ofn.lpstrDefExt = mDefaultExtension.get(); + } else if (IsDefaultPathHtml()) { + // Get file extension from suggested filename to detect if we are + // saving an html file. + // This is supposed to append ".htm" if user doesn't supply an + // extension but the behavior is sort of weird: + // - Often appends ".html" even if you have an extension + // - It obeys your extension if you put quotes around name + ofn.lpstrDefExt = htmExt.get(); + } + + // When possible, instead of using OFN_NOCHANGEDIR to ensure the current + // working directory will not change from this call, we will retrieve the + // current working directory before the call and restore it after the + // call. This flag causes problems on Windows XP for paths that are + // selected like C:test.txt where the user is currently at C:\somepath + // In which case expected result should be C:\somepath\test.txt + AutoRestoreWorkingPath restoreWorkingPath; + // If we can't get the current working directory, the best case is to + // use the OFN_NOCHANGEDIR flag + if (!restoreWorkingPath.HasWorkingPath()) { + ofn.Flags |= OFN_NOCHANGEDIR; + } + + bool result = false; + + switch(mMode) { + case modeOpen: + // FILE MUST EXIST! + ofn.Flags |= OFN_FILEMUSTEXIST; + result = FilePickerWrapper(&ofn, PICKER_TYPE_OPEN); + break; + + case modeOpenMultiple: + ofn.Flags |= OFN_FILEMUSTEXIST | OFN_ALLOWMULTISELECT; + + // The hook set here ensures that the buffer returned will always be + // large enough to hold all selected files. The hook may modify the + // value of ofn.lpstrFile and deallocate the old buffer that it pointed + // to (fileBuffer). The hook assumes that the passed in value is heap + // allocated and that the returned value should be freed by the caller. + // If the hook changes the buffer, it will deallocate the old buffer. + // This fix would be nice to have in Vista and up, but it would force + // the file picker to use the old style dialogs because hooks are not + // allowed in the new file picker UI. We need to eventually move to + // the new Common File Dialogs for Vista and up. + if (!IsVistaOrLater()) { + ofn.lpfnHook = MultiFilePickerHook; + fileBuffer.release(); + result = FilePickerWrapper(&ofn, PICKER_TYPE_OPEN); + fileBuffer.reset(ofn.lpstrFile); + } else { + result = FilePickerWrapper(&ofn, PICKER_TYPE_OPEN); + } + break; + + case modeSave: + { + ofn.Flags |= OFN_NOREADONLYRETURN; + + // Don't follow shortcuts when saving a shortcut, this can be used + // to trick users (bug 271732) + if (IsDefaultPathLink()) + ofn.Flags |= OFN_NODEREFERENCELINKS; + + result = FilePickerWrapper(&ofn, PICKER_TYPE_SAVE); + if (!result) { + // Error, find out what kind. + if (GetLastError() == ERROR_INVALID_PARAMETER || + CommDlgExtendedError() == FNERR_INVALIDFILENAME) { + // Probably the default file name is too long or contains illegal + // characters. Try again, without a starting file name. + ofn.lpstrFile[0] = L'\0'; + result = FilePickerWrapper(&ofn, PICKER_TYPE_SAVE); + } + } + } + break; + + default: + NS_NOTREACHED("unsupported file picker mode"); + return false; + } + + if (!result) + return false; + + // Remember what filter type the user selected + mSelectedType = (int16_t)ofn.nFilterIndex; + + // Single file selection, we're done + if (mMode != modeOpenMultiple) { + GetQualifiedPath(fileBuffer.get(), mUnicodeFile); + return true; + } + + // Set user-selected location of file or directory. From msdn's "Open and + // Save As Dialog Boxes" section: + // If you specify OFN_EXPLORER, the directory and file name strings are '\0' + // separated, with an extra '\0' character after the last file name. This + // format enables the Explorer-style dialog boxes to return long file names + // that include spaces. + wchar_t *current = fileBuffer.get(); + + nsAutoString dirName(current); + // Sometimes dirName contains a trailing slash and sometimes it doesn't: + if (current[dirName.Length() - 1] != '\\') + dirName.Append((char16_t)'\\'); + + while (current && *current && *(current + wcslen(current) + 1)) { + current = current + wcslen(current) + 1; + + nsCOMPtr<nsIFile> file = do_CreateInstance("@mozilla.org/file/local;1"); + NS_ENSURE_TRUE(file, false); + + // Only prepend the directory if the path specified is a relative path + nsAutoString path; + if (PathIsRelativeW(current)) { + path = dirName + nsDependentString(current); + } else { + path = current; + } + + nsAutoString canonicalizedPath; + GetQualifiedPath(path.get(), canonicalizedPath); + if (NS_FAILED(file->InitWithPath(canonicalizedPath)) || + !mFiles.AppendObject(file)) + return false; + } + + // Handle the case where the user selected just one file. From msdn: If you + // specify OFN_ALLOWMULTISELECT and the user selects only one file the + // lpstrFile string does not have a separator between the path and file name. + if (current && *current && (current == fileBuffer.get())) { + nsCOMPtr<nsIFile> file = do_CreateInstance("@mozilla.org/file/local;1"); + NS_ENSURE_TRUE(file, false); + + nsAutoString canonicalizedPath; + GetQualifiedPath(current, canonicalizedPath); + if (NS_FAILED(file->InitWithPath(canonicalizedPath)) || + !mFiles.AppendObject(file)) + return false; + } + + return true; +} + +/* + * Show a file picker post Windows XP + * + * @param aInitialDir The initial directory, the last used directory will be + * used if left blank. + * @param aWasInitError Out parameter will hold true if there was an error + * before the file picker is shown. + * @return true if a file was selected successfully. +*/ +bool +nsFilePicker::ShowFilePicker(const nsString& aInitialDir, bool &aWasInitError) +{ + PROFILER_LABEL_FUNC(js::ProfileEntry::Category::OTHER); + + if (!IsWin8OrLater()) { + // Some Windows 7 users are experiencing a race condition when some dlls + // that are loaded by the file picker cause a crash while attempting to shut + // down the COM multithreaded apartment. By instantiating EnsureMTA, we hold + // an additional reference to the MTA that should prevent this race, since + // the MTA will remain alive until shutdown. + EnsureMTA ensureMTA; + } + + RefPtr<IFileDialog> dialog; + if (mMode != modeSave) { + if (FAILED(CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC, + IID_IFileOpenDialog, + getter_AddRefs(dialog)))) { + aWasInitError = true; + return false; + } + } else { + if (FAILED(CoCreateInstance(CLSID_FileSaveDialog, nullptr, CLSCTX_INPROC, + IID_IFileSaveDialog, + getter_AddRefs(dialog)))) { + aWasInitError = true; + return false; + } + } + aWasInitError = false; + + // hook up event callbacks + dialog->Advise(this, &mFDECookie); + + // options + + FILEOPENDIALOGOPTIONS fos = 0; + fos |= FOS_SHAREAWARE | FOS_OVERWRITEPROMPT | + FOS_FORCEFILESYSTEM; + + // Handle add to recent docs settings + if (IsPrivacyModeEnabled() || !mAddToRecentDocs) { + fos |= FOS_DONTADDTORECENT; + } + + // Msdn claims FOS_NOCHANGEDIR is not needed. We'll add this + // just in case. + AutoRestoreWorkingPath arw; + + // mode specific + switch(mMode) { + case modeOpen: + fos |= FOS_FILEMUSTEXIST; + break; + + case modeOpenMultiple: + fos |= FOS_FILEMUSTEXIST | FOS_ALLOWMULTISELECT; + break; + + case modeSave: + fos |= FOS_NOREADONLYRETURN; + // Don't follow shortcuts when saving a shortcut, this can be used + // to trick users (bug 271732) + if (IsDefaultPathLink()) + fos |= FOS_NODEREFERENCELINKS; + break; + } + + dialog->SetOptions(fos); + + // initial strings + + // title + dialog->SetTitle(mTitle.get()); + + // default filename + if (!mDefaultFilename.IsEmpty()) { + dialog->SetFileName(mDefaultFilename.get()); + } + + NS_NAMED_LITERAL_STRING(htmExt, "html"); + + // default extension to append to new files + if (!mDefaultExtension.IsEmpty()) { + dialog->SetDefaultExtension(mDefaultExtension.get()); + } else if (IsDefaultPathHtml()) { + dialog->SetDefaultExtension(htmExt.get()); + } + + // initial location + if (!aInitialDir.IsEmpty()) { + RefPtr<IShellItem> folder; + if (SUCCEEDED( + WinUtils::SHCreateItemFromParsingName(aInitialDir.get(), nullptr, + IID_IShellItem, + getter_AddRefs(folder)))) { + dialog->SetFolder(folder); + } + } + + // filter types and the default index + if (!mComFilterList.IsEmpty()) { + dialog->SetFileTypes(mComFilterList.Length(), mComFilterList.get()); + dialog->SetFileTypeIndex(mSelectedType); + } + + // display + + { + AutoDestroyTmpWindow adtw((HWND)(mParentWidget.get() ? + mParentWidget->GetNativeData(NS_NATIVE_TMP_WINDOW) : nullptr)); + AutoTimerCallbackCancel atcc(this, PickerCallbackTimerFunc); + AutoWidgetPickerState awps(mParentWidget); + + if (FAILED(dialog->Show(adtw.get()))) { + dialog->Unadvise(mFDECookie); + return false; + } + dialog->Unadvise(mFDECookie); + } + + // results + + // Remember what filter type the user selected + UINT filterIdxResult; + if (SUCCEEDED(dialog->GetFileTypeIndex(&filterIdxResult))) { + mSelectedType = (int16_t)filterIdxResult; + } + + // single selection + if (mMode != modeOpenMultiple) { + RefPtr<IShellItem> item; + if (FAILED(dialog->GetResult(getter_AddRefs(item))) || !item) + return false; + return WinUtils::GetShellItemPath(item, mUnicodeFile); + } + + // multiple selection + RefPtr<IFileOpenDialog> openDlg; + dialog->QueryInterface(IID_IFileOpenDialog, getter_AddRefs(openDlg)); + if (!openDlg) { + // should not happen + return false; + } + + RefPtr<IShellItemArray> items; + if (FAILED(openDlg->GetResults(getter_AddRefs(items))) || !items) { + return false; + } + + DWORD count = 0; + items->GetCount(&count); + for (unsigned int idx = 0; idx < count; idx++) { + RefPtr<IShellItem> item; + nsAutoString str; + if (SUCCEEDED(items->GetItemAt(idx, getter_AddRefs(item)))) { + if (!WinUtils::GetShellItemPath(item, str)) + continue; + nsCOMPtr<nsIFile> file = do_CreateInstance("@mozilla.org/file/local;1"); + if (file && NS_SUCCEEDED(file->InitWithPath(str))) + mFiles.AppendObject(file); + } + } + return true; +} + +/////////////////////////////////////////////////////////////////////////////// +// nsIFilePicker impl. + +NS_IMETHODIMP +nsFilePicker::ShowW(int16_t *aReturnVal) +{ + NS_ENSURE_ARG_POINTER(aReturnVal); + + *aReturnVal = returnCancel; + + AutoSuppressEvents supress(mParentWidget); + + nsAutoString initialDir; + if (mDisplayDirectory) + mDisplayDirectory->GetPath(initialDir); + + // If no display directory, re-use the last one. + if(initialDir.IsEmpty()) { + // Allocate copy of last used dir. + initialDir = mLastUsedUnicodeDirectory; + } + + // Clear previous file selections + mUnicodeFile.Truncate(); + mFiles.Clear(); + + // On Win10, the picker doesn't support per-monitor DPI, so we open it + // with our context set temporarily to system-dpi-aware + WinUtils::AutoSystemDpiAware dpiAwareness; + + // Launch the XP file/folder picker on XP and as a fallback on Vista+. + // The CoCreateInstance call to CLSID_FileOpenDialog fails with "(0x80040111) + // ClassFactory cannot supply requested class" when the checkbox for + // Disable Visual Themes is on in the compatability tab within the shortcut + // properties. + bool result = false, wasInitError = true; + if (mMode == modeGetFolder) { + if (IsVistaOrLater()) + result = ShowFolderPicker(initialDir, wasInitError); + if (!result && wasInitError) + result = ShowXPFolderPicker(initialDir); + } else { + if (IsVistaOrLater()) + result = ShowFilePicker(initialDir, wasInitError); + if (!result && wasInitError) + result = ShowXPFilePicker(initialDir); + } + + // exit, and return returnCancel in aReturnVal + if (!result) + return NS_OK; + + RememberLastUsedDirectory(); + + int16_t retValue = returnOK; + if (mMode == modeSave) { + // Windows does not return resultReplace, we must check if file + // already exists. + nsCOMPtr<nsIFile> file(do_CreateInstance("@mozilla.org/file/local;1")); + bool flag = false; + if (file && NS_SUCCEEDED(file->InitWithPath(mUnicodeFile)) && + NS_SUCCEEDED(file->Exists(&flag)) && flag) { + retValue = returnReplace; + } + } + + *aReturnVal = retValue; + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::Show(int16_t *aReturnVal) +{ + return ShowW(aReturnVal); +} + +NS_IMETHODIMP +nsFilePicker::GetFile(nsIFile **aFile) +{ + NS_ENSURE_ARG_POINTER(aFile); + *aFile = nullptr; + + if (mUnicodeFile.IsEmpty()) + return NS_OK; + + nsCOMPtr<nsIFile> file(do_CreateInstance("@mozilla.org/file/local;1")); + + NS_ENSURE_TRUE(file, NS_ERROR_FAILURE); + + file->InitWithPath(mUnicodeFile); + + NS_ADDREF(*aFile = file); + + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::GetFileURL(nsIURI **aFileURL) +{ + *aFileURL = nullptr; + nsCOMPtr<nsIFile> file; + nsresult rv = GetFile(getter_AddRefs(file)); + if (!file) + return rv; + + return NS_NewFileURI(aFileURL, file); +} + +NS_IMETHODIMP +nsFilePicker::GetFiles(nsISimpleEnumerator **aFiles) +{ + NS_ENSURE_ARG_POINTER(aFiles); + return NS_NewArrayEnumerator(aFiles, mFiles); +} + +// Get the file + path +NS_IMETHODIMP +nsBaseWinFilePicker::SetDefaultString(const nsAString& aString) +{ + mDefaultFilePath = aString; + + // First, make sure the file name is not too long. + int32_t nameLength; + int32_t nameIndex = mDefaultFilePath.RFind("\\"); + if (nameIndex == kNotFound) + nameIndex = 0; + else + nameIndex ++; + nameLength = mDefaultFilePath.Length() - nameIndex; + mDefaultFilename.Assign(Substring(mDefaultFilePath, nameIndex)); + + if (nameLength > MAX_PATH) { + int32_t extIndex = mDefaultFilePath.RFind("."); + if (extIndex == kNotFound) + extIndex = mDefaultFilePath.Length(); + + // Let's try to shave the needed characters from the name part. + int32_t charsToRemove = nameLength - MAX_PATH; + if (extIndex - nameIndex >= charsToRemove) { + mDefaultFilePath.Cut(extIndex - charsToRemove, charsToRemove); + } + } + + // Then, we need to replace illegal characters. At this stage, we cannot + // replace the backslash as the string might represent a file path. + mDefaultFilePath.ReplaceChar(FILE_ILLEGAL_CHARACTERS, '-'); + mDefaultFilename.ReplaceChar(FILE_ILLEGAL_CHARACTERS, '-'); + + return NS_OK; +} + +NS_IMETHODIMP +nsBaseWinFilePicker::GetDefaultString(nsAString& aString) +{ + return NS_ERROR_FAILURE; +} + +// The default extension to use for files +NS_IMETHODIMP +nsBaseWinFilePicker::GetDefaultExtension(nsAString& aExtension) +{ + aExtension = mDefaultExtension; + return NS_OK; +} + +NS_IMETHODIMP +nsBaseWinFilePicker::SetDefaultExtension(const nsAString& aExtension) +{ + mDefaultExtension = aExtension; + return NS_OK; +} + +// Set the filter index +NS_IMETHODIMP +nsFilePicker::GetFilterIndex(int32_t *aFilterIndex) +{ + // Windows' filter index is 1-based, we use a 0-based system. + *aFilterIndex = mSelectedType - 1; + return NS_OK; +} + +NS_IMETHODIMP +nsFilePicker::SetFilterIndex(int32_t aFilterIndex) +{ + // Windows' filter index is 1-based, we use a 0-based system. + mSelectedType = aFilterIndex + 1; + return NS_OK; +} + +void +nsFilePicker::InitNative(nsIWidget *aParent, + const nsAString& aTitle) +{ + mParentWidget = aParent; + mTitle.Assign(aTitle); +} + +void +nsFilePicker::GetQualifiedPath(const wchar_t *aInPath, nsString &aOutPath) +{ + // Prefer a qualified path over a non qualified path. + // Things like c:file.txt would be accepted in Win XP but would later + // fail to open from the download manager. + wchar_t qualifiedFileBuffer[MAX_PATH]; + if (PathSearchAndQualifyW(aInPath, qualifiedFileBuffer, MAX_PATH)) { + aOutPath.Assign(qualifiedFileBuffer); + } else { + aOutPath.Assign(aInPath); + } +} + +void +nsFilePicker::AppendXPFilter(const nsAString& aTitle, const nsAString& aFilter) +{ + mFilterList.Append(aTitle); + mFilterList.Append(char16_t('\0')); + + if (aFilter.EqualsLiteral("..apps")) + mFilterList.AppendLiteral("*.exe;*.com"); + else + { + nsAutoString filter(aFilter); + filter.StripWhitespace(); + if (filter.EqualsLiteral("*")) + filter.AppendLiteral(".*"); + mFilterList.Append(filter); + } + + mFilterList.Append(char16_t('\0')); +} + +NS_IMETHODIMP +nsFilePicker::AppendFilter(const nsAString& aTitle, const nsAString& aFilter) +{ + if (IsVistaOrLater()) { + mComFilterList.Append(aTitle, aFilter); + } else { + AppendXPFilter(aTitle, aFilter); + } + return NS_OK; +} + +void +nsFilePicker::RememberLastUsedDirectory() +{ + nsCOMPtr<nsIFile> file(do_CreateInstance("@mozilla.org/file/local;1")); + if (!file || NS_FAILED(file->InitWithPath(mUnicodeFile))) { + NS_WARNING("RememberLastUsedDirectory failed to init file path."); + return; + } + + nsCOMPtr<nsIFile> dir; + nsAutoString newDir; + if (NS_FAILED(file->GetParent(getter_AddRefs(dir))) || + !(mDisplayDirectory = do_QueryInterface(dir)) || + NS_FAILED(mDisplayDirectory->GetPath(newDir)) || + newDir.IsEmpty()) { + NS_WARNING("RememberLastUsedDirectory failed to get parent directory."); + return; + } + + if (mLastUsedUnicodeDirectory) { + free(mLastUsedUnicodeDirectory); + mLastUsedUnicodeDirectory = nullptr; + } + mLastUsedUnicodeDirectory = ToNewUnicode(newDir); +} + +bool +nsFilePicker::IsPrivacyModeEnabled() +{ + return mLoadContext && mLoadContext->UsePrivateBrowsing(); +} + +bool +nsFilePicker::IsDefaultPathLink() +{ + NS_ConvertUTF16toUTF8 ext(mDefaultFilePath); + ext.Trim(" .", false, true); // watch out for trailing space and dots + ToLowerCase(ext); + if (StringEndsWith(ext, NS_LITERAL_CSTRING(".lnk")) || + StringEndsWith(ext, NS_LITERAL_CSTRING(".pif")) || + StringEndsWith(ext, NS_LITERAL_CSTRING(".url"))) + return true; + return false; +} + +bool +nsFilePicker::IsDefaultPathHtml() +{ + int32_t extIndex = mDefaultFilePath.RFind("."); + if (extIndex >= 0) { + nsAutoString ext; + mDefaultFilePath.Right(ext, mDefaultFilePath.Length() - extIndex); + if (ext.LowerCaseEqualsLiteral(".htm") || + ext.LowerCaseEqualsLiteral(".html") || + ext.LowerCaseEqualsLiteral(".shtml")) + return true; + } + return false; +} + +void +nsFilePicker::ComDlgFilterSpec::Append(const nsAString& aTitle, const nsAString& aFilter) +{ + COMDLG_FILTERSPEC* pSpecForward = mSpecList.AppendElement(); + if (!pSpecForward) { + NS_WARNING("mSpecList realloc failed."); + return; + } + memset(pSpecForward, 0, sizeof(*pSpecForward)); + nsString* pStr = mStrings.AppendElement(aTitle); + if (!pStr) { + NS_WARNING("mStrings.AppendElement failed."); + return; + } + pSpecForward->pszName = pStr->get(); + pStr = mStrings.AppendElement(aFilter); + if (!pStr) { + NS_WARNING("mStrings.AppendElement failed."); + return; + } + if (aFilter.EqualsLiteral("..apps")) + pStr->AssignLiteral("*.exe;*.com"); + else { + pStr->StripWhitespace(); + if (pStr->EqualsLiteral("*")) + pStr->AppendLiteral(".*"); + } + pSpecForward->pszSpec = pStr->get(); +} |