mirror of
https://github.com/ApfelTeeSaft/reactos.git
synced 2026-09-03 04:13:31 +00:00
Add library to deal with recycle bin.
It is aimed to be compatible with Windows 2000/XP/2003 (at least) on FAT or NTFS volumes. svn path=/trunk/; revision=21553
This commit is contained in:
@@ -4,13 +4,12 @@
|
||||
<directory name="3rdparty">
|
||||
<xi:include href="3rdparty/3rdparty.rbuild" />
|
||||
</directory>
|
||||
<directory name="drivers">
|
||||
<xi:include href="drivers/directory.rbuild" />
|
||||
</directory>
|
||||
|
||||
<directory name="crt">
|
||||
<xi:include href="crt/crt.rbuild" />
|
||||
</directory>
|
||||
<directory name="drivers">
|
||||
<xi:include href="drivers/directory.rbuild" />
|
||||
</directory>
|
||||
<directory name="dxguid">
|
||||
<xi:include href="dxguid/dxguid.rbuild" />
|
||||
</directory>
|
||||
@@ -38,6 +37,9 @@
|
||||
<directory name="pseh">
|
||||
<xi:include href="pseh/pseh.rbuild" />
|
||||
</directory>
|
||||
<directory name="recyclebin">
|
||||
<xi:include href="recyclebin/recyclebin.rbuild" />
|
||||
</directory>
|
||||
<directory name="rossym">
|
||||
<xi:include href="rossym/rossym.rbuild" />
|
||||
</directory>
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
/*
|
||||
* PROJECT: Recycle bin management
|
||||
* LICENSE: GPL v2 - See COPYING in the top level directory
|
||||
* FILE: lib/recyclebin/openclose.c
|
||||
* PURPOSE: Open/close recycle bins
|
||||
* PROGRAMMERS: Copyright 2006 Hervé Poussineau ([email protected])
|
||||
*/
|
||||
|
||||
#include "recyclebin_private.h"
|
||||
|
||||
BOOL
|
||||
IntCheckDeletedFileHandle(
|
||||
IN HANDLE hDeletedFile)
|
||||
{
|
||||
if (hDeletedFile == NULL || hDeletedFile == INVALID_HANDLE_VALUE)
|
||||
return FALSE;
|
||||
|
||||
if (((PDELETED_FILE_HANDLE)hDeletedFile)->magic != DELETEDFILE_MAGIC)
|
||||
return FALSE;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
IntCloseRecycleBinHandle(
|
||||
IN PREFCOUNT_DATA pData)
|
||||
{
|
||||
PRECYCLE_BIN bin;
|
||||
|
||||
bin = CONTAINING_RECORD(pData, RECYCLE_BIN, refCount);
|
||||
if (!CloseHandle(bin->hInfo))
|
||||
return FALSE;
|
||||
|
||||
RemoveEntryList(&bin->ListEntry);
|
||||
HeapFree(GetProcessHeap(), 0, bin);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
IntCreateEmptyRecycleBin(
|
||||
IN PRECYCLE_BIN bin,
|
||||
IN PSID OwnerSid OPTIONAL)
|
||||
{
|
||||
LPWSTR BufferName = NULL;
|
||||
LPWSTR Separator; /* Pointer into BufferName buffer */
|
||||
LPWSTR FileName; /* Pointer into BufferName buffer */
|
||||
LPCSTR DesktopIniContents = "[.ShellClassInfo]\r\nCLSID={645FF040-5081-101B-9F08-00AA002F954E}\r\n";
|
||||
DWORD Info2Contents[] = { 5, 0, 0, 0x320, 0 };
|
||||
SIZE_T BytesToWrite, BytesWritten, Needed;
|
||||
HANDLE hFile = INVALID_HANDLE_VALUE;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
Needed = (wcslen(bin->Folder) + 1 + max(wcslen(RECYCLE_BIN_FILE_NAME), wcslen(L"desktop.ini")) + 1) * sizeof(WCHAR);
|
||||
BufferName = HeapAlloc(GetProcessHeap(), 0, Needed);
|
||||
if (!BufferName)
|
||||
{
|
||||
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
wcscpy(BufferName, bin->Folder);
|
||||
Separator = wcsstr(&BufferName[3], L"\\");
|
||||
if (Separator)
|
||||
*Separator = UNICODE_NULL;
|
||||
ret = CreateDirectoryW(BufferName, NULL);
|
||||
if (!ret && GetLastError() != ERROR_ALREADY_EXISTS)
|
||||
goto cleanup;
|
||||
SetFileAttributesW(BufferName, FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_HIDDEN);
|
||||
if (Separator)
|
||||
{
|
||||
*Separator = L'\\';
|
||||
ret = CreateDirectoryW(BufferName, NULL);
|
||||
if (!ret && GetLastError() != ERROR_ALREADY_EXISTS)
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (OwnerSid)
|
||||
{
|
||||
//DWORD rc;
|
||||
|
||||
/* Add ACL to allow only user/SYSTEM to open it */
|
||||
/* FIXME: rc = SetNamedSecurityInfo(
|
||||
BufferName,
|
||||
SE_FILE_OBJECT,
|
||||
???,
|
||||
OwnerSid,
|
||||
NULL,
|
||||
???,
|
||||
???);
|
||||
if (rc != ERROR_SUCCESS)
|
||||
{
|
||||
SetLastError(rc);
|
||||
goto cleanup;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
wcscat(BufferName, L"\\");
|
||||
FileName = &BufferName[wcslen(BufferName)];
|
||||
|
||||
/* Create desktop.ini */
|
||||
wcscpy(FileName, L"desktop.ini");
|
||||
hFile = CreateFileW(BufferName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_HIDDEN, NULL);
|
||||
if (hFile == INVALID_HANDLE_VALUE)
|
||||
goto cleanup;
|
||||
BytesToWrite = strlen(DesktopIniContents);
|
||||
ret = WriteFile(hFile, DesktopIniContents, (DWORD)BytesToWrite, &BytesWritten, NULL);
|
||||
if (!ret)
|
||||
goto cleanup;
|
||||
if (BytesWritten != BytesToWrite)
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
CloseHandle(hFile);
|
||||
hFile = INVALID_HANDLE_VALUE;
|
||||
|
||||
/* Create empty INFO2 file */
|
||||
wcscpy(FileName, RECYCLE_BIN_FILE_NAME);
|
||||
hFile = CreateFileW(BufferName, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_HIDDEN, NULL);
|
||||
if (hFile == INVALID_HANDLE_VALUE)
|
||||
goto cleanup;
|
||||
BytesToWrite = sizeof(Info2Contents);
|
||||
ret = WriteFile(hFile, Info2Contents, (DWORD)BytesToWrite, &BytesWritten, NULL);
|
||||
if (!ret)
|
||||
goto cleanup;
|
||||
if (BytesWritten != BytesToWrite)
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
bin->hInfo = hFile;
|
||||
ret = TRUE;
|
||||
|
||||
cleanup:
|
||||
HeapFree(GetProcessHeap(), 0, BufferName);
|
||||
if (!ret)
|
||||
{
|
||||
if (hFile != INVALID_HANDLE_VALUE)
|
||||
CloseHandle(hFile);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
PRECYCLE_BIN
|
||||
IntReferenceRecycleBin(
|
||||
IN WCHAR driveLetter)
|
||||
{
|
||||
PLIST_ENTRY ListEntry;
|
||||
PRECYCLE_BIN bin = NULL, ret = NULL;
|
||||
WCHAR RootPath[4];
|
||||
DWORD FileSystemFlags;
|
||||
LPCWSTR RecycleBinDirectory;
|
||||
HANDLE tokenHandle = INVALID_HANDLE_VALUE;
|
||||
PTOKEN_USER TokenUserInfo = NULL;
|
||||
LPWSTR StringSid = NULL;
|
||||
SIZE_T Needed, DirectoryLength;
|
||||
INFO2_HEADER Header;
|
||||
BOOL AlreadyCreated = FALSE;
|
||||
DWORD bytesRead;
|
||||
|
||||
static LIST_ENTRY ListHead;
|
||||
static BOOL ListInitialized = FALSE;
|
||||
|
||||
if (!ListInitialized)
|
||||
{
|
||||
InitializeListHead(&ListHead);
|
||||
ListInitialized = TRUE;
|
||||
}
|
||||
|
||||
/* Search if the recycle bin has already been opened */
|
||||
driveLetter = toupper(driveLetter);
|
||||
ListEntry = ListHead.Flink;
|
||||
while (ListEntry != &ListHead)
|
||||
{
|
||||
bin = CONTAINING_RECORD(ListEntry, RECYCLE_BIN, ListEntry);
|
||||
if (bin->Folder[0] == driveLetter)
|
||||
{
|
||||
ReferenceHandle(&bin->refCount);
|
||||
return bin;
|
||||
}
|
||||
ListEntry = ListEntry->Flink;
|
||||
}
|
||||
bin = NULL;
|
||||
|
||||
/* We need to create a new recycle bin */
|
||||
|
||||
/* Get information about file system */
|
||||
wsprintfW(RootPath, L"%c:\\", driveLetter);
|
||||
if (!GetVolumeInformationW(
|
||||
RootPath,
|
||||
NULL,
|
||||
0,
|
||||
NULL,
|
||||
NULL,
|
||||
&FileSystemFlags,
|
||||
NULL,
|
||||
0))
|
||||
{
|
||||
goto cleanup;
|
||||
}
|
||||
if (!(FileSystemFlags & FILE_PERSISTENT_ACLS))
|
||||
RecycleBinDirectory = RECYCLE_BIN_DIRECTORY_WITHOUT_ACL;
|
||||
else
|
||||
{
|
||||
RecycleBinDirectory = RECYCLE_BIN_DIRECTORY_WITH_ACL;
|
||||
|
||||
/* Get user SID */
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &tokenHandle))
|
||||
goto cleanup;
|
||||
if (GetTokenInformation(tokenHandle, TokenUser, NULL, 0, &Needed))
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
|
||||
goto cleanup;
|
||||
TokenUserInfo = HeapAlloc(GetProcessHeap(), 0, Needed);
|
||||
if (!TokenUserInfo)
|
||||
{
|
||||
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
||||
goto cleanup;
|
||||
}
|
||||
if (!GetTokenInformation(tokenHandle, TokenUser, TokenUserInfo, (DWORD)Needed, &Needed))
|
||||
goto cleanup;
|
||||
if (!ConvertSidToStringSidW(TokenUserInfo->User.Sid, &StringSid))
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Create RECYCLEBIN structure */
|
||||
openfile:
|
||||
DirectoryLength = 3 + wcslen(RecycleBinDirectory) + 1;
|
||||
if (StringSid)
|
||||
DirectoryLength += wcslen(StringSid) + 1;
|
||||
Needed = FIELD_OFFSET(RECYCLE_BIN, Folder) + (2 * DirectoryLength + 2 + wcslen(RECYCLE_BIN_FILE_NAME))* sizeof(WCHAR);
|
||||
bin = HeapAlloc(GetProcessHeap(), 0, Needed);
|
||||
if (!bin)
|
||||
{
|
||||
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
||||
goto cleanup;
|
||||
}
|
||||
memset(bin, 0, Needed);
|
||||
InitializeHandle(&bin->refCount, IntCloseRecycleBinHandle);
|
||||
bin->magic = RECYCLEBIN_MAGIC;
|
||||
bin->hInfo = INVALID_HANDLE_VALUE;
|
||||
bin->InfoFile = &bin->Folder[DirectoryLength];
|
||||
bin->Folder[0] = driveLetter;
|
||||
bin->Folder[1] = '\0';
|
||||
wcscat(bin->Folder, L":\\");
|
||||
wcscat(bin->Folder, RecycleBinDirectory);
|
||||
if (StringSid)
|
||||
{
|
||||
wcscat(bin->Folder, L"\\");
|
||||
wcscat(bin->Folder, StringSid);
|
||||
}
|
||||
wcscpy(bin->InfoFile, bin->Folder);
|
||||
wcscat(bin->InfoFile, L"\\");
|
||||
wcscat(bin->InfoFile, RECYCLE_BIN_FILE_NAME);
|
||||
InsertTailList(&ListHead, &bin->ListEntry);
|
||||
|
||||
/* Open info file */
|
||||
bin->hInfo = CreateFileW(bin->InfoFile, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
|
||||
if (bin->hInfo == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
if (GetLastError() == ERROR_PATH_NOT_FOUND || GetLastError() == ERROR_FILE_NOT_FOUND)
|
||||
{
|
||||
if (!IntCreateEmptyRecycleBin(bin, TokenUserInfo ? TokenUserInfo->User.Sid : NULL))
|
||||
goto cleanup;
|
||||
AlreadyCreated = TRUE;
|
||||
}
|
||||
}
|
||||
if (bin->hInfo == INVALID_HANDLE_VALUE)
|
||||
goto cleanup;
|
||||
|
||||
if (SetFilePointer(bin->hInfo, 0, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER)
|
||||
goto cleanup;
|
||||
if (!ReadFile(bin->hInfo, &Header, sizeof(INFO2_HEADER), &bytesRead, NULL))
|
||||
goto cleanup;
|
||||
if (bytesRead != sizeof(INFO2_HEADER) && !AlreadyCreated)
|
||||
{
|
||||
/* Create a new file */
|
||||
if (!DereferenceHandle(&bin->refCount))
|
||||
goto cleanup;
|
||||
if (!DeleteFileW(bin->InfoFile))
|
||||
goto cleanup;
|
||||
goto openfile;
|
||||
}
|
||||
switch (Header.dwVersion)
|
||||
{
|
||||
case 5:
|
||||
InitializeCallbacks5(&bin->Callbacks);
|
||||
break;
|
||||
default:
|
||||
/* Unknown recycle bin version */
|
||||
SetLastError(ERROR_NOT_SUPPORTED);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
ret = bin;
|
||||
|
||||
cleanup:
|
||||
if (tokenHandle != INVALID_HANDLE_VALUE)
|
||||
CloseHandle(tokenHandle);
|
||||
HeapFree(GetProcessHeap(), 0, TokenUserInfo);
|
||||
if (StringSid)
|
||||
LocalFree(StringSid);
|
||||
if (!ret)
|
||||
{
|
||||
if (bin && bin->hInfo != INVALID_HANDLE_VALUE)
|
||||
DereferenceHandle(&bin->refCount);
|
||||
HeapFree(GetProcessHeap(), 0, bin);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
This library deals with Recycle bin.
|
||||
It is aimed to be compatible with Windows 2000/XP/2003 (at least) on FAT or NTFS volumes.
|
||||
|
||||
|
||||
TODO
|
||||
- Empty a recycle bin containing directories (v5)
|
||||
- Set security on recycle bin folder
|
||||
- Delete files > 4Gb
|
||||
|
||||
3 levels
|
||||
- 1: recyclebin.c: Public interface
|
||||
- 2a: openclose.c: Open/close recycle bins
|
||||
- 2b: refcount.c: Do reference counting on objects
|
||||
- 3: recyclebin_v5.c: Deals with recycle bins of Windows 2000/XP/2003
|
||||
@@ -0,0 +1,319 @@
|
||||
/*
|
||||
* PROJECT: Recycle bin management
|
||||
* LICENSE: GPL v2 - See COPYING in the top level directory
|
||||
* FILE: lib/recyclebin/openclose.c
|
||||
* PURPOSE: Public interface
|
||||
* PROGRAMMERS: Copyright 2006 Hervé Poussineau ([email protected])
|
||||
*/
|
||||
|
||||
#include "recyclebin_private.h"
|
||||
|
||||
typedef struct _ENUMERATE_RECYCLE_BIN_CONTEXT
|
||||
{
|
||||
PRECYCLE_BIN bin;
|
||||
PENUMERATE_RECYCLEBIN_CALLBACK pFnCallback;
|
||||
PVOID Context;
|
||||
} ENUMERATE_RECYCLE_BIN_CONTEXT, *PENUMERATE_RECYCLE_BIN_CONTEXT;
|
||||
|
||||
BOOL WINAPI
|
||||
CloseRecycleBinHandle(
|
||||
IN HANDLE hDeletedFile)
|
||||
{
|
||||
BOOL ret = FALSE;
|
||||
|
||||
if (!IntCheckDeletedFileHandle(hDeletedFile))
|
||||
SetLastError(ERROR_INVALID_HANDLE);
|
||||
else
|
||||
{
|
||||
PDELETED_FILE_HANDLE file = (PDELETED_FILE_HANDLE)hDeletedFile;
|
||||
ret = DereferenceHandle(&file->refCount);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
BOOL WINAPI
|
||||
DeleteFileToRecycleBinA(
|
||||
IN LPCSTR FileName)
|
||||
{
|
||||
int len;
|
||||
LPWSTR FileNameW = NULL;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
len = MultiByteToWideChar(CP_ACP, 0, FileName, -1, NULL, 0);
|
||||
if (len == 0)
|
||||
goto cleanup;
|
||||
FileNameW = HeapAlloc(GetProcessHeap(), 0, len * sizeof(WCHAR));
|
||||
if (!FileNameW)
|
||||
{
|
||||
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
||||
goto cleanup;
|
||||
}
|
||||
if (MultiByteToWideChar(CP_ACP, 0, FileName, -1, FileNameW, len) == 0)
|
||||
goto cleanup;
|
||||
|
||||
ret = DeleteFileToRecycleBinW(FileNameW);
|
||||
|
||||
cleanup:
|
||||
HeapFree(GetProcessHeap(), 0, FileNameW);
|
||||
return ret;
|
||||
}
|
||||
|
||||
BOOL WINAPI
|
||||
DeleteFileToRecycleBinW(
|
||||
IN LPCWSTR FileName)
|
||||
{
|
||||
LPWSTR FullFileName = NULL;
|
||||
DWORD dwBufferLength = 0;
|
||||
LPWSTR lpFilePart;
|
||||
DWORD len;
|
||||
PRECYCLE_BIN bin = NULL;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
/* Get full file name */
|
||||
while (TRUE)
|
||||
{
|
||||
len = GetFullPathNameW(FileName, dwBufferLength, FullFileName, &lpFilePart);
|
||||
if (len == 0)
|
||||
goto cleanup;
|
||||
else if (len < dwBufferLength)
|
||||
break;
|
||||
HeapFree(GetProcessHeap(), 0, FullFileName);
|
||||
dwBufferLength = len;
|
||||
FullFileName = HeapAlloc(GetProcessHeap(), 0, dwBufferLength * sizeof(WCHAR));
|
||||
if (!FullFileName)
|
||||
{
|
||||
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
if (!lpFilePart || dwBufferLength < 2 || FullFileName[1] != ':')
|
||||
{
|
||||
/* Only a directory name, or not a local file */
|
||||
SetLastError(ERROR_INVALID_NAME);
|
||||
}
|
||||
|
||||
/* Open recycle bin */
|
||||
bin = IntReferenceRecycleBin(FullFileName[0]);
|
||||
if (!bin)
|
||||
goto cleanup;
|
||||
|
||||
if (bin->Callbacks.DeleteFile)
|
||||
ret = bin->Callbacks.DeleteFile(bin, FullFileName, lpFilePart);
|
||||
else
|
||||
SetLastError(ERROR_NOT_SUPPORTED);
|
||||
|
||||
cleanup:
|
||||
HeapFree(GetProcessHeap(), 0, FullFileName);
|
||||
if (bin)
|
||||
DereferenceHandle(&bin->refCount);
|
||||
return ret;
|
||||
}
|
||||
|
||||
BOOL WINAPI
|
||||
EmptyRecycleBinA(
|
||||
IN CHAR driveLetter)
|
||||
{
|
||||
return EmptyRecycleBinW((WCHAR)driveLetter);
|
||||
}
|
||||
|
||||
BOOL WINAPI
|
||||
EmptyRecycleBinW(
|
||||
IN WCHAR driveLetter)
|
||||
{
|
||||
PRECYCLE_BIN bin = NULL;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
/* Open recycle bin */
|
||||
bin = IntReferenceRecycleBin(driveLetter);
|
||||
if (!bin)
|
||||
goto cleanup;
|
||||
|
||||
if (bin->Callbacks.EmptyRecycleBin)
|
||||
ret = bin->Callbacks.EmptyRecycleBin(&bin);
|
||||
else
|
||||
SetLastError(ERROR_NOT_SUPPORTED);
|
||||
|
||||
cleanup:
|
||||
if (bin)
|
||||
DereferenceHandle(&bin->refCount);
|
||||
return ret;
|
||||
}
|
||||
|
||||
BOOL WINAPI
|
||||
EnumerateRecycleBinA(
|
||||
IN CHAR driveLetter,
|
||||
IN PENUMERATE_RECYCLEBIN_CALLBACK pFnCallback,
|
||||
IN PVOID Context)
|
||||
{
|
||||
return EnumerateRecycleBinW((WCHAR)driveLetter, pFnCallback, Context);
|
||||
}
|
||||
|
||||
static BOOL
|
||||
IntCloseDeletedFileHandle(
|
||||
IN PREFCOUNT_DATA pData)
|
||||
{
|
||||
PDELETED_FILE_HANDLE file;
|
||||
|
||||
file = CONTAINING_RECORD(pData, DELETED_FILE_HANDLE, refCount);
|
||||
if (!DereferenceHandle(&file->bin->refCount))
|
||||
return FALSE;
|
||||
|
||||
file->magic = 0;
|
||||
HeapFree(GetProcessHeap(), 0, file);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
IntEnumerateRecycleBinCallback(
|
||||
IN PVOID Context,
|
||||
IN HANDLE hDeletedFile)
|
||||
{
|
||||
PENUMERATE_RECYCLE_BIN_CONTEXT CallbackContext = (PENUMERATE_RECYCLE_BIN_CONTEXT)Context;
|
||||
PDELETED_FILE_HANDLE DeletedFileHandle = NULL;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
DeletedFileHandle = HeapAlloc(GetProcessHeap(), 0, sizeof(DELETED_FILE_HANDLE));
|
||||
if (!DeletedFileHandle)
|
||||
{
|
||||
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
ReferenceHandle(&CallbackContext->bin->refCount);
|
||||
InitializeHandle(&DeletedFileHandle->refCount, IntCloseDeletedFileHandle);
|
||||
DeletedFileHandle->magic = DELETEDFILE_MAGIC;
|
||||
DeletedFileHandle->bin = CallbackContext->bin;
|
||||
DeletedFileHandle->hDeletedFile = hDeletedFile;
|
||||
|
||||
ret = CallbackContext->pFnCallback(CallbackContext->Context, DeletedFileHandle);
|
||||
|
||||
cleanup:
|
||||
if (!ret)
|
||||
{
|
||||
if (DeletedFileHandle)
|
||||
DereferenceHandle(&DeletedFileHandle->refCount);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
BOOL WINAPI
|
||||
EnumerateRecycleBinW(
|
||||
IN WCHAR driveLetter,
|
||||
IN PENUMERATE_RECYCLEBIN_CALLBACK pFnCallback,
|
||||
IN PVOID Context)
|
||||
{
|
||||
PRECYCLE_BIN bin = NULL;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
/* Open recycle bin */
|
||||
bin = IntReferenceRecycleBin(driveLetter);
|
||||
if (!bin)
|
||||
goto cleanup;
|
||||
|
||||
if (bin->Callbacks.EnumerateFiles)
|
||||
{
|
||||
ENUMERATE_RECYCLE_BIN_CONTEXT CallbackContext;
|
||||
|
||||
CallbackContext.bin = bin;
|
||||
CallbackContext.pFnCallback = pFnCallback;
|
||||
CallbackContext.Context = Context;
|
||||
|
||||
ret = bin->Callbacks.EnumerateFiles(bin, IntEnumerateRecycleBinCallback, &CallbackContext);
|
||||
}
|
||||
else
|
||||
SetLastError(ERROR_NOT_SUPPORTED);
|
||||
|
||||
cleanup:
|
||||
if (bin)
|
||||
DereferenceHandle(&bin->refCount);
|
||||
return ret;
|
||||
}
|
||||
|
||||
BOOL WINAPI
|
||||
GetDeletedFileDetailsA(
|
||||
IN HANDLE hDeletedFile,
|
||||
IN DWORD BufferSize,
|
||||
IN OUT PDELETED_FILE_DETAILS_A FileDetails,
|
||||
OUT LPDWORD RequiredSize OPTIONAL)
|
||||
{
|
||||
PDELETED_FILE_DETAILS_W FileDetailsW = NULL;
|
||||
DWORD BufferSizeW = 0;
|
||||
DWORD RequiredSizeW;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
if (BufferSize >= FIELD_OFFSET(DELETED_FILE_DETAILS_A, FileName))
|
||||
{
|
||||
BufferSizeW = FIELD_OFFSET(DELETED_FILE_DETAILS_W, FileName)
|
||||
+ (BufferSize - FIELD_OFFSET(DELETED_FILE_DETAILS_A, FileName)) * sizeof(WCHAR);
|
||||
}
|
||||
if (FileDetails && BufferSizeW)
|
||||
{
|
||||
FileDetailsW = HeapAlloc(GetProcessHeap(), 0, BufferSizeW);
|
||||
if (!FileDetailsW)
|
||||
{
|
||||
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
ret = GetDeletedFileDetailsW(hDeletedFile, BufferSizeW, FileDetailsW, &RequiredSizeW);
|
||||
if (!ret)
|
||||
goto cleanup;
|
||||
|
||||
if (FileDetails)
|
||||
{
|
||||
memcpy(FileDetails, FileDetailsW, FIELD_OFFSET(DELETED_FILE_DETAILS_A, FileName));
|
||||
if (0 == WideCharToMultiByte(CP_ACP, 0, FileDetailsW->FileName, -1, FileDetails->FileName, BufferSize - FIELD_OFFSET(DELETED_FILE_DETAILS_A, FileName), NULL, NULL))
|
||||
goto cleanup;
|
||||
}
|
||||
ret = TRUE;
|
||||
|
||||
cleanup:
|
||||
HeapFree(GetProcessHeap(), 0, FileDetailsW);
|
||||
return ret;
|
||||
}
|
||||
|
||||
BOOL WINAPI
|
||||
GetDeletedFileDetailsW(
|
||||
IN HANDLE hDeletedFile,
|
||||
IN DWORD BufferSize,
|
||||
IN OUT PDELETED_FILE_DETAILS_W FileDetails,
|
||||
OUT LPDWORD RequiredSize OPTIONAL)
|
||||
{
|
||||
BOOL ret = FALSE;
|
||||
|
||||
if (!IntCheckDeletedFileHandle(hDeletedFile))
|
||||
SetLastError(ERROR_INVALID_HANDLE);
|
||||
else
|
||||
{
|
||||
PDELETED_FILE_HANDLE DeletedFile = (PDELETED_FILE_HANDLE)hDeletedFile;
|
||||
if (DeletedFile->bin->Callbacks.GetDetails)
|
||||
ret = DeletedFile->bin->Callbacks.GetDetails(DeletedFile->bin, DeletedFile->hDeletedFile, BufferSize, FileDetails, RequiredSize);
|
||||
else
|
||||
SetLastError(ERROR_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
BOOL WINAPI
|
||||
RestoreFile(
|
||||
IN HANDLE hDeletedFile)
|
||||
{
|
||||
BOOL ret = FALSE;
|
||||
|
||||
if (!IntCheckDeletedFileHandle(hDeletedFile))
|
||||
SetLastError(ERROR_INVALID_HANDLE);
|
||||
else
|
||||
{
|
||||
PDELETED_FILE_HANDLE DeletedFile = (PDELETED_FILE_HANDLE)hDeletedFile;
|
||||
if (DeletedFile->bin->Callbacks.RestoreFile)
|
||||
ret = DeletedFile->bin->Callbacks.RestoreFile(DeletedFile->bin, DeletedFile->hDeletedFile);
|
||||
else
|
||||
SetLastError(ERROR_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
#ifndef __RECYCLEBIN_H
|
||||
#define __RECYCLEBIN_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <windows.h>
|
||||
#define ANY_SIZE 1
|
||||
|
||||
typedef struct _DELETED_FILE_DETAILS_A
|
||||
{
|
||||
FILETIME LastModification;
|
||||
FILETIME DeletionTime;
|
||||
LARGE_INTEGER FileSize;
|
||||
LARGE_INTEGER PhysicalFileSize;
|
||||
DWORD Attributes;
|
||||
CHAR FileName[ANY_SIZE];
|
||||
} DELETED_FILE_DETAILS_A, *PDELETED_FILE_DETAILS_A;
|
||||
typedef struct _DELETED_FILE_DETAILS_W
|
||||
{
|
||||
FILETIME LastModification;
|
||||
FILETIME DeletionTime;
|
||||
LARGE_INTEGER FileSize;
|
||||
LARGE_INTEGER PhysicalFileSize;
|
||||
DWORD Attributes;
|
||||
WCHAR FileName[ANY_SIZE];
|
||||
} DELETED_FILE_DETAILS_W, *PDELETED_FILE_DETAILS_W;
|
||||
#ifdef UNICODE
|
||||
#define DELETED_FILE_DETAILS DELETED_FILE_DETAILS_W
|
||||
#define PDELETED_FILE_DETAILS PDELETED_FILE_DETAILS_W
|
||||
#else
|
||||
#define DELETED_FILE_DETAILS DELETED_FILE_DETAILS_A
|
||||
#define PDELETED_FILE_DETAILS PDELETED_FILE_DETAILS_A
|
||||
#endif
|
||||
|
||||
typedef BOOL (WINAPI *PENUMERATE_RECYCLEBIN_CALLBACK)(IN PVOID Context, IN HANDLE hDeletedFile);
|
||||
|
||||
BOOL WINAPI
|
||||
CloseRecycleBinHandle(
|
||||
IN HANDLE hDeletedFile);
|
||||
|
||||
BOOL WINAPI
|
||||
DeleteFileToRecycleBinA(
|
||||
IN LPCSTR FileName);
|
||||
BOOL WINAPI
|
||||
DeleteFileToRecycleBinW(
|
||||
IN LPCWSTR FileName);
|
||||
#ifdef UNICODE
|
||||
#define DeleteFileToRecycleBin DeleteFileToRecycleBinW
|
||||
#else
|
||||
#define DeleteFileToRecycleBin DeleteFileToRecycleBinA
|
||||
#endif
|
||||
|
||||
BOOL WINAPI
|
||||
EmptyRecycleBinA(
|
||||
IN CHAR driveLetter);
|
||||
BOOL WINAPI
|
||||
EmptyRecycleBinW(
|
||||
IN WCHAR driveLetter);
|
||||
#ifdef UNICODE
|
||||
#define EmptyRecycleBin EmptyRecycleBinW
|
||||
#else
|
||||
#define EmptyRecycleBin EmptyRecycleBinA
|
||||
#endif
|
||||
|
||||
BOOL WINAPI
|
||||
EnumerateRecycleBinA(
|
||||
IN CHAR driveLetter,
|
||||
IN PENUMERATE_RECYCLEBIN_CALLBACK pFnCallback,
|
||||
IN PVOID Context);
|
||||
BOOL WINAPI
|
||||
EnumerateRecycleBinW(
|
||||
IN WCHAR driveLetter,
|
||||
IN PENUMERATE_RECYCLEBIN_CALLBACK pFnCallback,
|
||||
IN PVOID Context);
|
||||
#ifdef UNICODE
|
||||
#define EnumerateRecycleBin EnumerateRecycleBinW
|
||||
#else
|
||||
#define EnumerateRecycleBin EnumerateRecycleBinA
|
||||
#endif
|
||||
|
||||
BOOL WINAPI
|
||||
GetDeletedFileDetailsA(
|
||||
IN HANDLE hDeletedFile,
|
||||
IN DWORD BufferSize,
|
||||
IN OUT PDELETED_FILE_DETAILS_A FileDetails,
|
||||
OUT LPDWORD RequiredSize OPTIONAL);
|
||||
BOOL WINAPI
|
||||
GetDeletedFileDetailsW(
|
||||
IN HANDLE hDeletedFile,
|
||||
IN DWORD BufferSize,
|
||||
IN OUT PDELETED_FILE_DETAILS_W FileDetails,
|
||||
OUT LPDWORD RequiredSize OPTIONAL);
|
||||
#ifdef UNICODE
|
||||
#define GetDeletedFileDetails GetDeletedFileDetailsW
|
||||
#else
|
||||
#define GetDeletedFileDetails GetDeletedFileDetailsA
|
||||
#endif
|
||||
|
||||
BOOL WINAPI
|
||||
RestoreFile(
|
||||
IN HANDLE hDeletedFile);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __RECYCLEBIN_H */
|
||||
@@ -0,0 +1,7 @@
|
||||
<module name="recyclebin" type="staticlibrary">
|
||||
<define name="__USE_W32API" />
|
||||
<file>openclose.c</file>
|
||||
<file>recyclebin.c</file>
|
||||
<file>recyclebin_v5.c</file>
|
||||
<file>refcount.c</file>
|
||||
</module>
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "recyclebin.h"
|
||||
#include "sddl.h"
|
||||
|
||||
/* Defines */
|
||||
#define RECYCLEBIN_MAGIC 0x6e694252
|
||||
#define DELETEDFILE_MAGIC 0x6e694253
|
||||
|
||||
#define RECYCLE_BIN_DIRECTORY_WITH_ACL L"RECYCLER"
|
||||
#define RECYCLE_BIN_DIRECTORY_WITHOUT_ACL L"RECYCLED"
|
||||
#define RECYCLE_BIN_FILE_NAME L"INFO2"
|
||||
|
||||
#define ROUND_UP(N, S) ((( (N) + (S) - 1) / (S) ) * (S) )
|
||||
|
||||
/* List manipulation */
|
||||
#define InitializeListHead(le) (void)((le)->Flink = (le)->Blink = (le))
|
||||
#define InsertTailList(le,e) do { PLIST_ENTRY b = (le)->Blink; (e)->Flink = (le); (e)->Blink = b; b->Flink = (e); (le)->Blink = (e); } while (0)
|
||||
#define RemoveEntryList(Entry) { PLIST_ENTRY _EX_Blink, _EX_Flink; _EX_Flink = (Entry)->Flink; _EX_Blink = (Entry)->Blink; _EX_Blink->Flink = _EX_Flink; _EX_Flink->Blink = _EX_Blink; }
|
||||
|
||||
/* Typedefs */
|
||||
struct _RECYCLE_BIN;
|
||||
typedef struct _RECYCLE_BIN *PRECYCLE_BIN;
|
||||
struct _REFCOUNT_DATA;
|
||||
typedef struct _REFCOUNT_DATA *PREFCOUNT_DATA;
|
||||
|
||||
typedef BOOL (*PINT_ENUMERATE_RECYCLEBIN_CALLBACK)(IN PVOID Context, IN HANDLE hDeletedFile);
|
||||
typedef BOOL (*PDESTROY_DATA) (IN PREFCOUNT_DATA pData);
|
||||
|
||||
typedef BOOL (*PCLOSE_HANDLE) (IN HANDLE hHandle);
|
||||
typedef BOOL (*PDELETE_FILE) (IN PRECYCLE_BIN bin, IN LPCWSTR FullPath, IN LPCWSTR FileName);
|
||||
typedef BOOL (*PEMPTY_RECYCLEBIN)(IN PRECYCLE_BIN* bin);
|
||||
typedef BOOL (*PENUMERATE_FILES) (IN PRECYCLE_BIN bin, IN PINT_ENUMERATE_RECYCLEBIN_CALLBACK pFnCallback, IN PVOID Context);
|
||||
typedef BOOL (*PGET_DETAILS) (IN PRECYCLE_BIN bin, IN HANDLE hDeletedFile, IN DWORD BufferSize, IN OUT PDELETED_FILE_DETAILS_W FileDetails, OUT LPDWORD RequiredSize OPTIONAL);
|
||||
typedef BOOL (*PRESTORE_FILE) (IN PRECYCLE_BIN bin, IN HANDLE hDeletedFile);
|
||||
|
||||
typedef struct _RECYCLEBIN_CALLBACKS
|
||||
{
|
||||
PCLOSE_HANDLE CloseHandle;
|
||||
PDELETE_FILE DeleteFile;
|
||||
PEMPTY_RECYCLEBIN EmptyRecycleBin;
|
||||
PENUMERATE_FILES EnumerateFiles;
|
||||
PGET_DETAILS GetDetails;
|
||||
PRESTORE_FILE RestoreFile;
|
||||
} RECYCLEBIN_CALLBACKS, *PRECYCLEBIN_CALLBACKS;
|
||||
|
||||
typedef struct _REFCOUNT_DATA
|
||||
{
|
||||
DWORD ReferenceCount;
|
||||
PDESTROY_DATA Close;
|
||||
} REFCOUNT_DATA;
|
||||
|
||||
typedef struct _RECYCLE_BIN
|
||||
{
|
||||
DWORD magic; /* RECYCLEBIN_MAGIC */
|
||||
LIST_ENTRY ListEntry;
|
||||
REFCOUNT_DATA refCount;
|
||||
HANDLE hInfo;
|
||||
RECYCLEBIN_CALLBACKS Callbacks;
|
||||
LPWSTR InfoFile;
|
||||
WCHAR Folder[ANY_SIZE]; /* [drive]:\[RECYCLE_BIN_DIRECTORY]\{SID} */
|
||||
} RECYCLE_BIN;
|
||||
|
||||
typedef struct _DELETED_FILE_HANDLE
|
||||
{
|
||||
DWORD magic; /* DELETEDFILE_MAGIC */
|
||||
REFCOUNT_DATA refCount;
|
||||
PRECYCLE_BIN bin;
|
||||
HANDLE hDeletedFile; /* specific to recycle bin format */
|
||||
} DELETED_FILE_HANDLE, *PDELETED_FILE_HANDLE;
|
||||
|
||||
/* Structures on disk */
|
||||
|
||||
#include <pshpack1.h>
|
||||
|
||||
typedef struct _INFO2_HEADER
|
||||
{
|
||||
DWORD dwVersion;
|
||||
DWORD dwNumberOfEntries;
|
||||
DWORD dwHighestRecordUniqueId;
|
||||
DWORD dwRecordSize;
|
||||
DWORD dwTotalLogicalSize;
|
||||
} INFO2_HEADER, *PINFO2_HEADER;
|
||||
|
||||
#include <poppack.h>
|
||||
|
||||
/* Prototypes */
|
||||
|
||||
/* openclose.c */
|
||||
|
||||
BOOL
|
||||
IntCheckDeletedFileHandle(
|
||||
IN HANDLE hDeletedFile);
|
||||
|
||||
PRECYCLE_BIN
|
||||
IntReferenceRecycleBin(
|
||||
IN WCHAR driveLetter);
|
||||
|
||||
/* recyclebin_v5.c */
|
||||
|
||||
VOID
|
||||
InitializeCallbacks5(
|
||||
IN OUT PRECYCLEBIN_CALLBACKS Callbacks);
|
||||
|
||||
/* refcount.c */
|
||||
|
||||
BOOL
|
||||
InitializeHandle(
|
||||
IN PREFCOUNT_DATA pData,
|
||||
IN PDESTROY_DATA pFnClose OPTIONAL);
|
||||
|
||||
BOOL
|
||||
ReferenceHandle(
|
||||
IN PREFCOUNT_DATA pData);
|
||||
|
||||
BOOL
|
||||
DereferenceHandle(
|
||||
IN PREFCOUNT_DATA pData);
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* PROJECT: Recycle bin management
|
||||
* LICENSE: GPL v2 - See COPYING in the top level directory
|
||||
* FILE: lib/recyclebin/openclose.c
|
||||
* PURPOSE: Deals with recycle bins of Windows 2000/XP/2003
|
||||
* PROGRAMMERS: Copyright 2006 Hervé Poussineau ([email protected])
|
||||
*/
|
||||
|
||||
#include "recyclebin_v5.h"
|
||||
|
||||
VOID
|
||||
InitializeCallbacks5(
|
||||
IN OUT PRECYCLEBIN_CALLBACKS Callbacks)
|
||||
{
|
||||
Callbacks->CloseHandle = CloseHandle5;
|
||||
Callbacks->DeleteFile = DeleteFile5;
|
||||
Callbacks->EmptyRecycleBin = EmptyRecycleBin5;
|
||||
Callbacks->EnumerateFiles = EnumerateFiles5;
|
||||
Callbacks->GetDetails = GetDetails5;
|
||||
Callbacks->RestoreFile = RestoreFile5;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
CloseHandle5(
|
||||
IN HANDLE hDeletedFile)
|
||||
{
|
||||
/* Nothing to do, as hDeletedFile is simply a DWORD... */
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
DeleteFile5(
|
||||
IN PRECYCLE_BIN bin,
|
||||
IN LPCWSTR FullPath,
|
||||
IN LPCWSTR FileName)
|
||||
{
|
||||
HANDLE hFile = INVALID_HANDLE_VALUE;
|
||||
INFO2_HEADER Header;
|
||||
DELETED_FILE_RECORD DeletedFile;
|
||||
DWORD bytesRead, bytesWritten;
|
||||
LARGE_INTEGER fileSize;
|
||||
SYSTEMTIME SystemTime;
|
||||
WCHAR RootDir[4];
|
||||
WCHAR DeletedFileName[2 * MAX_PATH];
|
||||
LPCWSTR Extension;
|
||||
DWORD ClusterSize, BytesPerSector, SectorsPerCluster;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
if (wcslen(FullPath) >= MAX_PATH)
|
||||
{
|
||||
/* Unable to store a too long path in recycle bin */
|
||||
SetLastError(ERROR_INVALID_NAME);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
hFile = CreateFileW(FullPath, 0, 0, NULL, OPEN_EXISTING, 0, NULL);
|
||||
if (hFile == INVALID_HANDLE_VALUE)
|
||||
goto cleanup;
|
||||
|
||||
if (SetFilePointer(bin->hInfo, 0, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER)
|
||||
goto cleanup;
|
||||
if (!ReadFile(bin->hInfo, &Header, sizeof(INFO2_HEADER), &bytesRead, NULL))
|
||||
goto cleanup;
|
||||
if (bytesRead != sizeof(INFO2_HEADER) || Header.dwRecordSize == 0)
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (Header.dwVersion != 5 || Header.dwRecordSize != sizeof(DELETED_FILE_RECORD))
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Get file size */
|
||||
#if 0
|
||||
if (!GetFileSizeEx(hFile, &fileSize))
|
||||
goto cleanup;
|
||||
#else
|
||||
fileSize.u.LowPart = GetFileSize(hFile, &fileSize.u.HighPart);
|
||||
if (fileSize.u.LowPart == INVALID_FILE_SIZE && GetLastError() != NO_ERROR)
|
||||
goto cleanup;
|
||||
#endif
|
||||
/* Check if file size is > 4Gb */
|
||||
if (fileSize.u.HighPart != 0)
|
||||
{
|
||||
/* FIXME: how to delete files >= 4Gb? */
|
||||
SetLastError(ERROR_CALL_NOT_IMPLEMENTED);
|
||||
goto cleanup;
|
||||
}
|
||||
Header.dwTotalLogicalSize += fileSize.u.LowPart;
|
||||
|
||||
/* Generate new name */
|
||||
Header.dwHighestRecordUniqueId++;
|
||||
Extension = wcsrchr(FileName, '.');
|
||||
wsprintfW(DeletedFileName, L"%s\\D%c%lu%s", bin->Folder, FullPath[0] - 'A' + 'a', Header.dwHighestRecordUniqueId, Extension);
|
||||
|
||||
/* Get cluster size */
|
||||
wsprintfW(RootDir, L"%c:\\", bin->Folder[0]);
|
||||
if (!GetDiskFreeSpaceW(RootDir, &SectorsPerCluster, &BytesPerSector, NULL, NULL))
|
||||
goto cleanup;
|
||||
ClusterSize = BytesPerSector * SectorsPerCluster;
|
||||
|
||||
/* Get current time */
|
||||
GetSystemTime(&SystemTime);
|
||||
if (!SystemTimeToFileTime(&SystemTime, &DeletedFile.DeletionTime))
|
||||
goto cleanup;
|
||||
|
||||
/* Update INFO2 */
|
||||
memset(&DeletedFile, 0, sizeof(DELETED_FILE_RECORD));
|
||||
if (WideCharToMultiByte(CP_ACP, 0, FullPath, -1, DeletedFile.FileNameA, MAX_PATH, NULL, NULL) == 0)
|
||||
{
|
||||
SetLastError(ERROR_INVALID_NAME);
|
||||
goto cleanup;
|
||||
}
|
||||
DeletedFile.dwRecordUniqueId = Header.dwHighestRecordUniqueId;
|
||||
DeletedFile.dwDriveNumber = tolower(bin->Folder[0]) - 'a';
|
||||
DeletedFile.dwPhysicalFileSize = ROUND_UP(fileSize.u.LowPart, ClusterSize);
|
||||
wcscpy(DeletedFile.FileNameW, FullPath);
|
||||
|
||||
if (!SetFilePointer(bin->hInfo, 0, NULL, FILE_END) == INVALID_SET_FILE_POINTER)
|
||||
goto cleanup;
|
||||
if (!WriteFile(bin->hInfo, &DeletedFile, sizeof(DELETED_FILE_RECORD), &bytesWritten, NULL))
|
||||
goto cleanup;
|
||||
if (bytesWritten != sizeof(DELETED_FILE_RECORD))
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
Header.dwNumberOfEntries++;
|
||||
if (SetFilePointer(bin->hInfo, 0, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER)
|
||||
{
|
||||
goto cleanup;
|
||||
}
|
||||
if (!WriteFile(bin->hInfo, &Header, sizeof(INFO2_HEADER), &bytesWritten, NULL))
|
||||
goto cleanup;
|
||||
if (bytesWritten != sizeof(INFO2_HEADER))
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Move file */
|
||||
if (!MoveFileW(FullPath, DeletedFileName))
|
||||
goto cleanup;
|
||||
|
||||
ret = TRUE;
|
||||
|
||||
cleanup:
|
||||
if (hFile != INVALID_HANDLE_VALUE)
|
||||
CloseHandle(hFile);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
EmptyRecycleBin5(
|
||||
IN PRECYCLE_BIN* bin)
|
||||
{
|
||||
LPWSTR InfoFile = NULL;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
InfoFile = HeapAlloc(GetProcessHeap(), 0, wcslen((*bin)->InfoFile) * sizeof(WCHAR) + sizeof(UNICODE_NULL));
|
||||
if (!InfoFile)
|
||||
{
|
||||
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
||||
goto cleanup;
|
||||
}
|
||||
wcscpy(InfoFile, (*bin)->InfoFile);
|
||||
|
||||
/* Delete all files in the recycle bin */
|
||||
if (!EnumerateFiles5(*bin, IntEmptyRecycleBinCallback, *bin))
|
||||
goto cleanup;
|
||||
|
||||
/* Delete INFO2 */
|
||||
if (!DereferenceHandle(&(*bin)->refCount))
|
||||
goto cleanup;
|
||||
if (!DeleteFileW(InfoFile))
|
||||
goto cleanup;
|
||||
*bin = NULL;
|
||||
ret = TRUE;
|
||||
|
||||
cleanup:
|
||||
HeapFree(GetProcessHeap(), 0, InfoFile);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
EnumerateFiles5(
|
||||
IN PRECYCLE_BIN bin,
|
||||
IN PINT_ENUMERATE_RECYCLEBIN_CALLBACK pFnCallback,
|
||||
IN PVOID Context)
|
||||
{
|
||||
INFO2_HEADER Header;
|
||||
DELETED_FILE_RECORD DeletedFile;
|
||||
DWORD bytesRead, dwEntries;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
if (SetFilePointer(bin->hInfo, 0, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER)
|
||||
goto cleanup;
|
||||
if (!ReadFile(bin->hInfo, &Header, sizeof(INFO2_HEADER), &bytesRead, NULL))
|
||||
goto cleanup;
|
||||
if (bytesRead != sizeof(INFO2_HEADER) || Header.dwRecordSize == 0)
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (Header.dwVersion != 5 || Header.dwRecordSize != sizeof(DELETED_FILE_RECORD))
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
SetLastError(ERROR_SUCCESS);
|
||||
for (dwEntries = 0; dwEntries < Header.dwNumberOfEntries; dwEntries++)
|
||||
{
|
||||
if (!ReadFile(bin->hInfo, &DeletedFile, Header.dwRecordSize, &bytesRead, NULL))
|
||||
goto cleanup;
|
||||
if (bytesRead != Header.dwRecordSize)
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
if (!pFnCallback(Context, (HANDLE)(ULONG_PTR)DeletedFile.dwRecordUniqueId))
|
||||
goto cleanup;
|
||||
if (SetFilePointer(bin->hInfo, sizeof(INFO2_HEADER) + Header.dwRecordSize * dwEntries, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER)
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
ret = TRUE;
|
||||
|
||||
cleanup:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
GetDetails5(
|
||||
IN PRECYCLE_BIN bin,
|
||||
IN HANDLE hDeletedFile,
|
||||
IN DWORD BufferSize,
|
||||
IN OUT PDELETED_FILE_DETAILS_W FileDetails,
|
||||
OUT LPDWORD RequiredSize OPTIONAL)
|
||||
{
|
||||
DELETED_FILE_RECORD DeletedFile;
|
||||
SIZE_T Needed;
|
||||
LPWSTR FullName = NULL;
|
||||
HANDLE hFile = INVALID_HANDLE_VALUE;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
if (!IntSearchRecord(bin, hDeletedFile, &DeletedFile, NULL))
|
||||
goto cleanup;
|
||||
Needed = (DWORD)FIELD_OFFSET(DELETED_FILE_DETAILS_W, FileName) + (wcslen(DeletedFile.FileNameW) + 1) * sizeof(WCHAR);
|
||||
if (RequiredSize)
|
||||
*RequiredSize = (DWORD)Needed;
|
||||
if (Needed > BufferSize)
|
||||
{
|
||||
SetLastError(ERROR_INSUFFICIENT_BUFFER);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (!IntGetFullName(bin, &DeletedFile, &FullName))
|
||||
goto cleanup;
|
||||
|
||||
/* Open file */
|
||||
FileDetails->Attributes = GetFileAttributesW(FullName);
|
||||
if (FileDetails->Attributes == INVALID_FILE_ATTRIBUTES)
|
||||
goto cleanup;
|
||||
if (FileDetails->Attributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||
hFile = CreateFileW(FullName, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||||
else
|
||||
hFile = CreateFileW(FullName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
|
||||
if (hFile == INVALID_HANDLE_VALUE)
|
||||
goto cleanup;
|
||||
|
||||
/* Fill returned structure */
|
||||
if (!GetFileTime(hFile, NULL, NULL, &FileDetails->LastModification))
|
||||
goto cleanup;
|
||||
memcpy(&FileDetails->DeletionTime, &DeletedFile.DeletionTime, sizeof(FILETIME));
|
||||
FileDetails->FileSize.u.LowPart = GetFileSize(hFile, &FileDetails->FileSize.u.HighPart);
|
||||
if (FileDetails->FileSize.u.LowPart == INVALID_FILE_SIZE)
|
||||
goto cleanup;
|
||||
FileDetails->PhysicalFileSize.u.HighPart = 0;
|
||||
FileDetails->PhysicalFileSize.u.LowPart = DeletedFile.dwPhysicalFileSize;
|
||||
wcscpy(FileDetails->FileName, DeletedFile.FileNameW);
|
||||
|
||||
ret = TRUE;
|
||||
|
||||
cleanup:
|
||||
HeapFree(GetProcessHeap(), 0, FullName);
|
||||
if (hFile != INVALID_HANDLE_VALUE)
|
||||
CloseHandle(hFile);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
RestoreFile5(
|
||||
IN PRECYCLE_BIN bin,
|
||||
IN HANDLE hDeletedFile)
|
||||
{
|
||||
INFO2_HEADER Header;
|
||||
DWORD bytesRead, bytesWritten;
|
||||
LARGE_INTEGER Position;
|
||||
DELETED_FILE_RECORD DeletedFile, LastFile;
|
||||
LPWSTR FullName = NULL;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
if (SetFilePointer(bin->hInfo, 0, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER)
|
||||
goto cleanup;
|
||||
if (!ReadFile(bin->hInfo, &Header, sizeof(INFO2_HEADER), &bytesRead, NULL))
|
||||
goto cleanup;
|
||||
if (bytesRead != sizeof(INFO2_HEADER) || Header.dwRecordSize == 0)
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (Header.dwVersion != 5 || Header.dwRecordSize != sizeof(DELETED_FILE_RECORD))
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Search deleted entry */
|
||||
if (!IntSearchRecord(bin, hDeletedFile, &DeletedFile, &Position))
|
||||
goto cleanup;
|
||||
/* Get destination full name */
|
||||
if (!IntGetFullName(bin, &DeletedFile, &FullName))
|
||||
goto cleanup;
|
||||
/* Restore file */
|
||||
if (!MoveFileW(FullName, DeletedFile.FileNameW))
|
||||
goto cleanup;
|
||||
|
||||
/* Update INFO2 */
|
||||
/* 1) If not last entry, copy last entry to the current one */
|
||||
if (SetFilePointer(bin->hInfo, -sizeof(DELETED_FILE_RECORD), NULL, FILE_END) == INVALID_SET_FILE_POINTER)
|
||||
goto cleanup;
|
||||
if (!ReadFile(bin->hInfo, &LastFile, sizeof(DELETED_FILE_RECORD), &bytesRead, NULL))
|
||||
goto cleanup;
|
||||
if (bytesRead != sizeof(DELETED_FILE_RECORD))
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
if (LastFile.dwRecordUniqueId != DeletedFile.dwRecordUniqueId)
|
||||
{
|
||||
/* Move the last entry to the current one */
|
||||
if (SetFilePointer(bin->hInfo, Position.u.LowPart, &Position.u.HighPart, FILE_BEGIN) == INVALID_SET_FILE_POINTER)
|
||||
goto cleanup;
|
||||
if (!WriteFile(bin->hInfo, &LastFile, sizeof(DELETED_FILE_RECORD), &bytesWritten, NULL))
|
||||
goto cleanup;
|
||||
if (bytesWritten != sizeof(DELETED_FILE_RECORD))
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
/* 2) Update the header */
|
||||
Header.dwNumberOfEntries--;
|
||||
if (SetFilePointer(bin->hInfo, 0, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER)
|
||||
goto cleanup;
|
||||
if (!WriteFile(bin->hInfo, &Header, sizeof(INFO2_HEADER), &bytesWritten, NULL))
|
||||
goto cleanup;
|
||||
if (bytesWritten != sizeof(INFO2_HEADER))
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
/* 3) Truncate file */
|
||||
if (SetFilePointer(bin->hInfo, -sizeof(DELETED_FILE_RECORD), NULL, FILE_END) == INVALID_SET_FILE_POINTER)
|
||||
goto cleanup;
|
||||
if (!SetEndOfFile(bin->hInfo))
|
||||
goto cleanup;
|
||||
ret = TRUE;
|
||||
|
||||
cleanup:
|
||||
HeapFree(GetProcessHeap(), 0, FullName);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
IntDeleteRecursive(
|
||||
IN LPCWSTR FullName)
|
||||
{
|
||||
DWORD RemovableAttributes = FILE_ATTRIBUTE_READONLY;
|
||||
DWORD FileAttributes;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
FileAttributes = GetFileAttributesW(FullName);
|
||||
if (FileAttributes == INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
if (GetLastError() == ERROR_FILE_NOT_FOUND)
|
||||
ret = TRUE;
|
||||
goto cleanup;
|
||||
}
|
||||
if (FileAttributes & RemovableAttributes)
|
||||
{
|
||||
if (!SetFileAttributesW(FullName, FileAttributes & ~RemovableAttributes))
|
||||
goto cleanup;
|
||||
}
|
||||
if (FileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||
{
|
||||
/* Recursive deletion */
|
||||
/* FIXME: recursive deletion */
|
||||
SetLastError(ERROR_CALL_NOT_IMPLEMENTED);
|
||||
goto cleanup;
|
||||
|
||||
if (!RemoveDirectoryW(FullName))
|
||||
goto cleanup;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!DeleteFileW(FullName))
|
||||
goto cleanup;
|
||||
}
|
||||
ret = TRUE;
|
||||
|
||||
cleanup:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
IntEmptyRecycleBinCallback(
|
||||
IN PVOID Context,
|
||||
IN HANDLE hDeletedFile)
|
||||
{
|
||||
PRECYCLE_BIN bin = (PRECYCLE_BIN)Context;
|
||||
DELETED_FILE_RECORD DeletedFile;
|
||||
LPWSTR FullName = NULL;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
if (!IntSearchRecord(bin, hDeletedFile, &DeletedFile, NULL))
|
||||
goto cleanup;
|
||||
|
||||
if (!IntGetFullName(bin, &DeletedFile, &FullName))
|
||||
goto cleanup;
|
||||
|
||||
if (!IntDeleteRecursive(FullName))
|
||||
goto cleanup;
|
||||
ret = TRUE;
|
||||
|
||||
cleanup:
|
||||
HeapFree(GetProcessHeap(), 0, FullName);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
IntGetFullName(
|
||||
IN PRECYCLE_BIN bin,
|
||||
IN PDELETED_FILE_RECORD pDeletedFile,
|
||||
OUT LPWSTR* pFullName)
|
||||
{
|
||||
SIZE_T Needed;
|
||||
LPCWSTR Extension;
|
||||
LPWSTR FullName = NULL;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
*pFullName = NULL;
|
||||
Extension = wcsrchr(pDeletedFile->FileNameW, '.');
|
||||
if (Extension < wcsrchr(pDeletedFile->FileNameW, '\\'))
|
||||
Extension = NULL;
|
||||
Needed = wcslen(bin->Folder) + 13;
|
||||
if (Extension)
|
||||
Needed += wcslen(Extension);
|
||||
FullName = HeapAlloc(GetProcessHeap(), 0, Needed * sizeof(WCHAR));
|
||||
if (!FullName)
|
||||
{
|
||||
SetLastError(ERROR_NOT_ENOUGH_MEMORY);
|
||||
goto cleanup;
|
||||
}
|
||||
wsprintfW(FullName, L"%s\\D%c%lu%s", bin->Folder, pDeletedFile->dwDriveNumber + 'a', pDeletedFile->dwRecordUniqueId, Extension);
|
||||
*pFullName = FullName;
|
||||
ret = TRUE;
|
||||
|
||||
cleanup:
|
||||
if (!ret)
|
||||
HeapFree(GetProcessHeap(), 0, FullName);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static BOOL
|
||||
IntSearchRecord(
|
||||
IN PRECYCLE_BIN bin,
|
||||
IN HANDLE hDeletedFile,
|
||||
OUT PDELETED_FILE_RECORD pDeletedFile,
|
||||
OUT PLARGE_INTEGER Position OPTIONAL)
|
||||
{
|
||||
INFO2_HEADER Header;
|
||||
DELETED_FILE_RECORD DeletedFile;
|
||||
DWORD bytesRead, dwEntries;
|
||||
BOOL ret = FALSE;
|
||||
|
||||
if (SetFilePointer(bin->hInfo, 0, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER)
|
||||
goto cleanup;
|
||||
if (!ReadFile(bin->hInfo, &Header, sizeof(INFO2_HEADER), &bytesRead, NULL))
|
||||
goto cleanup;
|
||||
if (bytesRead != sizeof(INFO2_HEADER) || Header.dwRecordSize == 0)
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (Header.dwVersion != 5 || Header.dwRecordSize != sizeof(DELETED_FILE_RECORD))
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
SetLastError(ERROR_SUCCESS);
|
||||
for (dwEntries = 0; dwEntries < Header.dwNumberOfEntries; dwEntries++)
|
||||
{
|
||||
if (Position)
|
||||
{
|
||||
LARGE_INTEGER Zero;
|
||||
Zero.QuadPart = 0;
|
||||
if (!SetFilePointerEx(bin->hInfo, Zero, Position, FILE_CURRENT))
|
||||
goto cleanup;
|
||||
}
|
||||
if (!ReadFile(bin->hInfo, &DeletedFile, Header.dwRecordSize, &bytesRead, NULL))
|
||||
goto cleanup;
|
||||
if (bytesRead != Header.dwRecordSize)
|
||||
{
|
||||
SetLastError(ERROR_GEN_FAILURE);
|
||||
goto cleanup;
|
||||
}
|
||||
if (DeletedFile.dwRecordUniqueId == (DWORD)(ULONG_PTR)hDeletedFile)
|
||||
{
|
||||
memcpy(pDeletedFile, &DeletedFile, Header.dwRecordSize);
|
||||
ret = TRUE;
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
/* Entry not found */
|
||||
SetLastError(ERROR_INVALID_HANDLE);
|
||||
|
||||
cleanup:
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/* Recycle bin management
|
||||
* This file is under the GPLv2 licence
|
||||
* Copyright (C) 2006 Hervé Poussineau <[email protected]>
|
||||
*/
|
||||
|
||||
#include "recyclebin_private.h"
|
||||
|
||||
#include <pshpack1.h>
|
||||
|
||||
/* MS Windows 2000/XP/2003 */
|
||||
typedef struct _DELETED_FILE_RECORD
|
||||
{
|
||||
CHAR FileNameA[MAX_PATH];
|
||||
DWORD dwRecordUniqueId;
|
||||
DWORD dwDriveNumber;
|
||||
FILETIME DeletionTime;
|
||||
DWORD dwPhysicalFileSize;
|
||||
WCHAR FileNameW[MAX_PATH];
|
||||
} DELETED_FILE_RECORD, *PDELETED_FILE_RECORD;
|
||||
|
||||
#include <poppack.h>
|
||||
|
||||
static BOOL
|
||||
CloseHandle5(
|
||||
IN HANDLE hDeletedFile);
|
||||
|
||||
static BOOL
|
||||
DeleteFile5(
|
||||
IN PRECYCLE_BIN Context,
|
||||
IN LPCWSTR FullPath,
|
||||
IN LPCWSTR FileName);
|
||||
|
||||
static BOOL
|
||||
EmptyRecycleBin5(
|
||||
IN PRECYCLE_BIN* bin);
|
||||
|
||||
static BOOL
|
||||
EnumerateFiles5(
|
||||
IN PRECYCLE_BIN bin,
|
||||
IN PINT_ENUMERATE_RECYCLEBIN_CALLBACK pFnCallback,
|
||||
IN PVOID Context);
|
||||
|
||||
static BOOL
|
||||
GetDetails5(
|
||||
IN PRECYCLE_BIN bin,
|
||||
IN HANDLE hDeletedFile,
|
||||
IN DWORD BufferSize,
|
||||
IN OUT PDELETED_FILE_DETAILS_W FileDetails,
|
||||
OUT LPDWORD RequiredSize OPTIONAL);
|
||||
|
||||
static BOOL
|
||||
RestoreFile5(
|
||||
IN PRECYCLE_BIN Context,
|
||||
IN HANDLE hDeletedFile);
|
||||
|
||||
static BOOL
|
||||
IntDeleteRecursive(
|
||||
IN LPCWSTR FullName);
|
||||
|
||||
static BOOL
|
||||
IntEmptyRecycleBinCallback(
|
||||
IN PVOID Context,
|
||||
IN HANDLE hDeletedFile);
|
||||
|
||||
static BOOL
|
||||
IntGetFullName(
|
||||
IN PRECYCLE_BIN bin,
|
||||
IN PDELETED_FILE_RECORD pDeletedFile,
|
||||
OUT LPWSTR* pFullName);
|
||||
|
||||
static BOOL
|
||||
IntSearchRecord(
|
||||
IN PRECYCLE_BIN bin,
|
||||
IN HANDLE hDeletedFile,
|
||||
OUT PDELETED_FILE_RECORD DeletedFile,
|
||||
OUT PLARGE_INTEGER Position OPTIONAL);
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* PROJECT: Recycle bin management
|
||||
* LICENSE: GPL v2 - See COPYING in the top level directory
|
||||
* FILE: lib/recyclebin/openclose.c
|
||||
* PURPOSE: Do reference counting on objects
|
||||
* PROGRAMMERS: Copyright 2006 Hervé Poussineau ([email protected])
|
||||
*/
|
||||
|
||||
#include "recyclebin_private.h"
|
||||
|
||||
BOOL
|
||||
InitializeHandle(
|
||||
IN PREFCOUNT_DATA pData,
|
||||
IN PDESTROY_DATA pFnClose OPTIONAL)
|
||||
{
|
||||
pData->ReferenceCount = 1;
|
||||
pData->Close = pFnClose;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL
|
||||
ReferenceHandle(
|
||||
IN PREFCOUNT_DATA pData)
|
||||
{
|
||||
pData->ReferenceCount++;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
BOOL
|
||||
DereferenceHandle(
|
||||
IN PREFCOUNT_DATA pData)
|
||||
{
|
||||
pData->ReferenceCount--;
|
||||
if (pData->ReferenceCount == 0)
|
||||
{
|
||||
if (pData->Close)
|
||||
return pData->Close(pData);
|
||||
else
|
||||
return TRUE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
Reference in New Issue
Block a user