Fixing MoveItemToTrash behavior in Windows

Modifying MoveItemToTrash for Windows to actually verify file can be
moved to a drive's recycle bin before attempting to delete it.

PR: #3337.
This commit is contained in:
Tyler Gibson 2015-11-04 13:40:12 -08:00 committed by Cheng Zhao
parent 4ce840d8e6
commit 4ff5ce4d6d

View file

@ -5,7 +5,9 @@
#include "atom/common/platform_util.h"
#include <windows.h>
#include <atlbase.h>
#include <commdlg.h>
#include <comdef.h>
#include <dwmapi.h>
#include <shellapi.h>
#include <shlobj.h>
@ -19,6 +21,7 @@
#include "base/strings/utf_string_conversions.h"
#include "base/win/registry.h"
#include "base/win/scoped_co_mem.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/scoped_comptr.h"
#include "base/win/windows_version.h"
#include "url/gurl.h"
@ -42,6 +45,159 @@ bool ValidateShellCommandForScheme(const std::string& scheme) {
return true;
}
// Required COM implementation of IFileOperationProgressSink so we can
// precheck files before deletion to make sure they can be move to the
// Recycle Bin.
class DeleteFileProgressSink : public IFileOperationProgressSink {
public:
DeleteFileProgressSink();
private:
ULONG STDMETHODCALLTYPE AddRef(void);
ULONG STDMETHODCALLTYPE Release(void);
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID* ppvObj);
HRESULT STDMETHODCALLTYPE StartOperations(void);
HRESULT STDMETHODCALLTYPE FinishOperations(HRESULT);
HRESULT STDMETHODCALLTYPE PreRenameItem(
DWORD, IShellItem*, LPCWSTR);
HRESULT STDMETHODCALLTYPE PostRenameItem(
DWORD, IShellItem*, LPCWSTR, HRESULT, IShellItem*);
HRESULT STDMETHODCALLTYPE PreMoveItem(
DWORD, IShellItem*, IShellItem*, LPCWSTR);
HRESULT STDMETHODCALLTYPE PostMoveItem(
DWORD, IShellItem*, IShellItem*, LPCWSTR, HRESULT, IShellItem*);
HRESULT STDMETHODCALLTYPE PreCopyItem(
DWORD, IShellItem*, IShellItem*, LPCWSTR);
HRESULT STDMETHODCALLTYPE PostCopyItem(
DWORD, IShellItem*, IShellItem*, LPCWSTR, HRESULT, IShellItem*);
HRESULT STDMETHODCALLTYPE PreDeleteItem(DWORD, IShellItem*);
HRESULT STDMETHODCALLTYPE PostDeleteItem(
DWORD, IShellItem*, HRESULT, IShellItem*);
HRESULT STDMETHODCALLTYPE PreNewItem(
DWORD, IShellItem*, LPCWSTR);
HRESULT STDMETHODCALLTYPE PostNewItem(
DWORD, IShellItem*, LPCWSTR, LPCWSTR, DWORD, HRESULT, IShellItem*);
HRESULT STDMETHODCALLTYPE UpdateProgress(UINT, UINT);
HRESULT STDMETHODCALLTYPE ResetTimer(void);
HRESULT STDMETHODCALLTYPE PauseTimer(void);
HRESULT STDMETHODCALLTYPE ResumeTimer(void);
ULONG m_cRef;
};
DeleteFileProgressSink::DeleteFileProgressSink() {
m_cRef = 0;
}
HRESULT DeleteFileProgressSink::PreDeleteItem(DWORD dwFlags, IShellItem*) {
if (!(dwFlags & TSF_DELETE_RECYCLE_IF_POSSIBLE)) {
// TSF_DELETE_RECYCLE_IF_POSSIBLE will not be set for items that cannot be
// recycled. In this case, we abort the delete operation. This bubbles
// up and stops the Delete in IFileOperation.
return E_ABORT;
}
// Returns S_OK if successful, or an error value otherwise. In the case of an
// error value, the delete operation and all subsequent operations pending
// from the call to IFileOperation are canceled.
return S_OK;
}
HRESULT DeleteFileProgressSink::QueryInterface(REFIID riid, LPVOID* ppvObj) {
// Always set out parameter to NULL, validating it first.
if (!ppvObj)
return E_INVALIDARG;
*ppvObj = nullptr;
if (riid == IID_IUnknown || riid == IID_IFileOperationProgressSink) {
// Increment the reference count and return the pointer.
*ppvObj = reinterpret_cast<IUnknown*>(this);
AddRef();
return NOERROR;
}
return E_NOINTERFACE;
}
ULONG DeleteFileProgressSink::AddRef() {
InterlockedIncrement(&m_cRef);
return m_cRef;
}
ULONG DeleteFileProgressSink::Release() {
// Decrement the object's internal counter.
ULONG ulRefCount = InterlockedDecrement(&m_cRef);
if (0 == m_cRef) {
delete this;
}
return ulRefCount;
}
HRESULT DeleteFileProgressSink::StartOperations() {
return S_OK;
}
HRESULT DeleteFileProgressSink::FinishOperations(HRESULT) {
return S_OK;
}
HRESULT DeleteFileProgressSink::PreRenameItem(DWORD, IShellItem*, LPCWSTR) {
return S_OK;
}
HRESULT DeleteFileProgressSink::PostRenameItem(
DWORD, IShellItem*, __RPC__in_string LPCWSTR, HRESULT, IShellItem*) {
return E_NOTIMPL;
}
HRESULT DeleteFileProgressSink::PreMoveItem(
DWORD, IShellItem*, IShellItem*, LPCWSTR) {
return E_NOTIMPL;
}
HRESULT DeleteFileProgressSink::PostMoveItem(
DWORD, IShellItem*, IShellItem*, LPCWSTR, HRESULT, IShellItem*) {
return E_NOTIMPL;
}
HRESULT DeleteFileProgressSink::PreCopyItem(
DWORD, IShellItem*, IShellItem*, LPCWSTR) {
return E_NOTIMPL;
}
HRESULT DeleteFileProgressSink::PostCopyItem(
DWORD, IShellItem*, IShellItem*, LPCWSTR, HRESULT, IShellItem*) {
return E_NOTIMPL;
}
HRESULT DeleteFileProgressSink::PostDeleteItem(
DWORD, IShellItem*, HRESULT, IShellItem*) {
return S_OK;
}
HRESULT DeleteFileProgressSink::PreNewItem(
DWORD dwFlags, IShellItem*, LPCWSTR) {
return E_NOTIMPL;
}
HRESULT DeleteFileProgressSink::PostNewItem(
DWORD, IShellItem*, LPCWSTR, LPCWSTR, DWORD, HRESULT, IShellItem*) {
return E_NOTIMPL;
}
HRESULT DeleteFileProgressSink::UpdateProgress(UINT, UINT) {
return S_OK;
}
HRESULT DeleteFileProgressSink::ResetTimer() {
return S_OK;
}
HRESULT DeleteFileProgressSink::PauseTimer() {
return S_OK;
}
HRESULT DeleteFileProgressSink::ResumeTimer() {
return S_OK;
}
} // namespace
namespace platform_util {
@ -170,32 +326,40 @@ bool OpenExternal(const GURL& url) {
}
bool MoveItemToTrash(const base::FilePath& path) {
// SHFILEOPSTRUCT wants the path to be terminated with two NULLs,
// so we have to use wcscpy because wcscpy_s writes non-NULLs
// into the rest of the buffer.
wchar_t double_terminated_path[MAX_PATH + 1] = {0};
#pragma warning(suppress:4996) // don't complain about wcscpy deprecation
wcscpy(double_terminated_path, path.value().c_str());
SHFILEOPSTRUCT file_operation = {0};
file_operation.wFunc = FO_DELETE;
file_operation.pFrom = double_terminated_path;
file_operation.fFlags = FOF_ALLOWUNDO | FOF_SILENT | FOF_NOCONFIRMATION;
int err = SHFileOperation(&file_operation);
// Since we're passing flags to the operation telling it to be silent,
// it's possible for the operation to be aborted/cancelled without err
// being set (although MSDN doesn't give any scenarios for how this can
// happen). See MSDN for SHFileOperation and SHFILEOPTSTRUCT.
if (file_operation.fAnyOperationsAborted)
base::win::ScopedCOMInitializer com_initializer;
if (!com_initializer.succeeded())
return false;
// Some versions of Windows return ERROR_FILE_NOT_FOUND (0x2) when deleting
// an empty directory and some return 0x402 when they should be returning
// ERROR_FILE_NOT_FOUND. MSDN says Vista and up won't return 0x402. Windows 7
// can return DE_INVALIDFILES (0x7C) for nonexistent directories.
return (err == 0 || err == ERROR_FILE_NOT_FOUND || err == 0x402 ||
err == 0x7C);
base::win::ScopedComPtr<IFileOperation> pfo;
if (FAILED(pfo.CreateInstance(CLSID_FileOperation)))
return false;
// Elevation prompt enabled for UAC protected files. This overrides the
// SILENT, NO_UI and NOERRORUI flags.
if (FAILED(pfo->SetOperationFlags(FOF_NO_UI |
FOF_ALLOWUNDO |
FOF_NOERRORUI |
FOF_SILENT |
FOFX_SHOWELEVATIONPROMPT |
FOFX_RECYCLEONDELETE)))
return false;
// Create an IShellItem from the supplied source path.
base::win::ScopedComPtr<IShellItem> delete_item;
if (FAILED(SHCreateItemFromParsingName(path.value().c_str(),
NULL,
IID_PPV_ARGS(delete_item.Receive()))))
return false;
base::win::ScopedComPtr<IFileOperationProgressSink> delete_sink(
new DeleteFileProgressSink);
if (!delete_sink)
return false;
// Processes the queued command DeleteItem. This will trigger
// the DeleteFileProgressSink to check for Recycle Bin.
return SUCCEEDED(pfo->DeleteItem(delete_item.get(), delete_sink.get())) &&
SUCCEEDED(pfo->PerformOperations());
}
void Beep() {