mirror of
https://github.com/winfsp/winfsp.git
synced 2025-07-02 08:52:55 -05:00
sys,dll,inc: implement new Delete design and POSIX semantics
This commit is contained in:
@ -82,21 +82,21 @@ static NTSTATUS FspFsvolCleanup(
|
||||
FSP_FILE_DESC *FileDesc = FileObject->FsContext2;
|
||||
FSP_FSCTL_TRANSACT_REQ *Request;
|
||||
ULONG CleanupFlags;
|
||||
BOOLEAN DeletePending, SetAllocationSize, FileModified;
|
||||
BOOLEAN Delete, SetAllocationSize, FileModified;
|
||||
|
||||
ASSERT(FileNode == FileDesc->FileNode);
|
||||
|
||||
FspFileNodeAcquireExclusive(FileNode, Main);
|
||||
|
||||
FspFileNodeCleanup(FileNode, FileObject, &CleanupFlags);
|
||||
DeletePending = CleanupFlags & 1;
|
||||
Delete = (CleanupFlags & 1) && !FileNode->PosixDelete;
|
||||
SetAllocationSize = !!(CleanupFlags & 2);
|
||||
FileModified = BooleanFlagOn(FileObject->Flags, FO_FILE_MODIFIED);
|
||||
|
||||
/* if this is a directory inform the FSRTL Notify mechanism */
|
||||
if (FileNode->IsDirectory)
|
||||
{
|
||||
if (DeletePending)
|
||||
if (Delete)
|
||||
FspNotifyDeletePending(
|
||||
FsvolDeviceExtension->NotifySync, &FsvolDeviceExtension->NotifyList, FileNode);
|
||||
|
||||
@ -108,12 +108,12 @@ static NTSTATUS FspFsvolCleanup(
|
||||
FspFileNodeUnlockAll(FileNode, FileObject, IoGetRequestorProcess(Irp));
|
||||
|
||||
/* create the user-mode file system request; MustSucceed because IRP_MJ_CLEANUP cannot fail */
|
||||
FspIopCreateRequestMustSucceedEx(Irp, DeletePending ? &FileNode->FileName : 0, 0,
|
||||
FspIopCreateRequestMustSucceedEx(Irp, Delete ? &FileNode->FileName : 0, 0,
|
||||
FspFsvolCleanupRequestFini, &Request);
|
||||
Request->Kind = FspFsctlTransactCleanupKind;
|
||||
Request->Req.Cleanup.UserContext = FileNode->UserContext;
|
||||
Request->Req.Cleanup.UserContext2 = FileDesc->UserContext2;
|
||||
Request->Req.Cleanup.Delete = DeletePending;
|
||||
Request->Req.Cleanup.Delete = Delete;
|
||||
Request->Req.Cleanup.SetAllocationSize = SetAllocationSize;
|
||||
Request->Req.Cleanup.SetArchiveBit = (FileModified || FileDesc->DidSetSecurity) &&
|
||||
!FileDesc->DidSetFileAttributes;
|
||||
@ -170,7 +170,12 @@ NTSTATUS FspFsvolCleanupComplete(
|
||||
ASSERT(FileNode == FileDesc->FileNode);
|
||||
|
||||
/* send the appropriate notification; also invalidate dirinfo/etc. caches */
|
||||
if (Request->Req.Cleanup.Delete)
|
||||
if (FileNode->PosixDelete)
|
||||
{
|
||||
NotifyFilter = 0;
|
||||
NotifyAction = 0;
|
||||
}
|
||||
else if (Request->Req.Cleanup.Delete)
|
||||
{
|
||||
NotifyFilter = FileNode->IsDirectory ?
|
||||
FILE_NOTIFY_CHANGE_DIR_NAME : FILE_NOTIFY_CHANGE_FILE_NAME;
|
||||
|
@ -1462,6 +1462,7 @@ typedef struct FSP_FILE_NODE
|
||||
ULONG EaChangeNumber;
|
||||
ULONG EaChangeCount;
|
||||
BOOLEAN TruncateOnClose;
|
||||
BOOLEAN PosixDelete;
|
||||
FILE_LOCK FileLock;
|
||||
#if (NTDDI_VERSION < NTDDI_WIN8)
|
||||
OPLOCK Oplock;
|
||||
@ -1561,6 +1562,7 @@ NTSTATUS FspFileNodeOpen(FSP_FILE_NODE *FileNode, PFILE_OBJECT FileObject,
|
||||
VOID FspFileNodeCleanup(FSP_FILE_NODE *FileNode, PFILE_OBJECT FileObject, PULONG PCleanupFlags);
|
||||
VOID FspFileNodeCleanupFlush(FSP_FILE_NODE *FileNode, PFILE_OBJECT FileObject);
|
||||
VOID FspFileNodeCleanupComplete(FSP_FILE_NODE *FileNode, PFILE_OBJECT FileObject);
|
||||
VOID FspFileNodePosixDelete(FSP_FILE_NODE *FileNode, PFILE_OBJECT FileObject);
|
||||
VOID FspFileNodeClose(FSP_FILE_NODE *FileNode,
|
||||
PFILE_OBJECT FileObject, /* non-0 to remove share access */
|
||||
BOOLEAN HandleCleanup); /* TRUE to decrement handle count */
|
||||
|
@ -43,6 +43,7 @@ NTSTATUS FspFileNodeOpen(FSP_FILE_NODE *FileNode, PFILE_OBJECT FileObject,
|
||||
VOID FspFileNodeCleanup(FSP_FILE_NODE *FileNode, PFILE_OBJECT FileObject, PULONG PCleanupFlags);
|
||||
VOID FspFileNodeCleanupFlush(FSP_FILE_NODE *FileNode, PFILE_OBJECT FileObject);
|
||||
VOID FspFileNodeCleanupComplete(FSP_FILE_NODE *FileNode, PFILE_OBJECT FileObject);
|
||||
VOID FspFileNodePosixDelete(FSP_FILE_NODE *FileNode, PFILE_OBJECT FileObject);
|
||||
VOID FspFileNodeClose(FSP_FILE_NODE *FileNode,
|
||||
PFILE_OBJECT FileObject, /* non-0 to remove share access */
|
||||
BOOLEAN HandleCleanup); /* TRUE to decrement handle count */
|
||||
@ -141,6 +142,7 @@ VOID FspFileNodeOplockComplete(PVOID Context, PIRP Irp);
|
||||
#pragma alloc_text(PAGE, FspFileNodeCleanup)
|
||||
#pragma alloc_text(PAGE, FspFileNodeCleanupFlush)
|
||||
#pragma alloc_text(PAGE, FspFileNodeCleanupComplete)
|
||||
#pragma alloc_text(PAGE, FspFileNodePosixDelete)
|
||||
#pragma alloc_text(PAGE, FspFileNodeClose)
|
||||
#pragma alloc_text(PAGE, FspFileNodeFlushAndPurgeCache)
|
||||
#pragma alloc_text(PAGE, FspFileNodeOverwriteStreams)
|
||||
@ -909,7 +911,7 @@ VOID FspFileNodeCleanupComplete(FSP_FILE_NODE *FileNode, PFILE_OBJECT FileObject
|
||||
DeletePending = 0 != FileNode->DeletePending;
|
||||
MemoryBarrier();
|
||||
|
||||
if (DeletePending)
|
||||
if (DeletePending && !FileNode->PosixDelete)
|
||||
{
|
||||
FspFsvolDeviceDeleteContextByName(FsvolDeviceObject, &FileNode->FileName,
|
||||
&DeletedFromContextTable);
|
||||
@ -1013,6 +1015,66 @@ VOID FspFileNodeCleanupComplete(FSP_FILE_NODE *FileNode, PFILE_OBJECT FileObject
|
||||
FspFileNodeDereference(FileNode);
|
||||
}
|
||||
|
||||
VOID FspFileNodePosixDelete(FSP_FILE_NODE *FileNode, PFILE_OBJECT FileObject)
|
||||
{
|
||||
/*
|
||||
* Perform a POSIX delete of a FileNode. This removes the FileNode from the Context table.
|
||||
*
|
||||
* The FileNode must be acquired exclusive (Main or Full) when calling this function.
|
||||
*/
|
||||
|
||||
PAGED_CODE();
|
||||
|
||||
PDEVICE_OBJECT FsvolDeviceObject = FileNode->FsvolDeviceObject;
|
||||
FSP_FSVOL_DEVICE_EXTENSION *FsvolDeviceExtension = FspFsvolDeviceExtension(FsvolDeviceObject);
|
||||
BOOLEAN DeletedFromContextTable = FALSE;
|
||||
|
||||
FspFsvolDeviceLockContextTable(FsvolDeviceObject);
|
||||
|
||||
FspFsvolDeviceDeleteContextByName(FsvolDeviceObject, &FileNode->FileName,
|
||||
&DeletedFromContextTable);
|
||||
ASSERT(DeletedFromContextTable);
|
||||
|
||||
FileNode->OpenCount = 0;
|
||||
|
||||
if (FsvolDeviceExtension->VolumeParams.NamedStreams &&
|
||||
0 == FileNode->MainFileNode)
|
||||
{
|
||||
BOOLEAN StreamDeletedFromContextTable;
|
||||
USHORT FileNameLength = FileNode->FileName.Length;
|
||||
|
||||
GATHER_DESCENDANTS(&FileNode->FileName, FALSE,
|
||||
if (DescendantFileNode->FileName.Length > FileNameLength &&
|
||||
L'\\' == DescendantFileNode->FileName.Buffer[FileNameLength / sizeof(WCHAR)])
|
||||
break;
|
||||
ASSERT(FileNode != DescendantFileNode);
|
||||
ASSERT(0 != DescendantFileNode->OpenCount);
|
||||
);
|
||||
|
||||
for (
|
||||
DescendantFileNodeIndex = 0;
|
||||
DescendantFileNodeCount > DescendantFileNodeIndex;
|
||||
DescendantFileNodeIndex++)
|
||||
{
|
||||
DescendantFileNode = DescendantFileNodes[DescendantFileNodeIndex];
|
||||
|
||||
FspFsvolDeviceDeleteContextByName(FsvolDeviceObject, &DescendantFileNode->FileName,
|
||||
&StreamDeletedFromContextTable);
|
||||
if (StreamDeletedFromContextTable)
|
||||
{
|
||||
DescendantFileNode->OpenCount = 0;
|
||||
FspFileNodeDereference(DescendantFileNode);
|
||||
}
|
||||
}
|
||||
|
||||
SCATTER_DESCENDANTS(FALSE);
|
||||
}
|
||||
|
||||
FspFsvolDeviceUnlockContextTable(FsvolDeviceObject);
|
||||
|
||||
FspFileNodeDereference(FileNode);
|
||||
}
|
||||
|
||||
VOID FspFileNodeClose(FSP_FILE_NODE *FileNode,
|
||||
PFILE_OBJECT FileObject, /* non-0 to remove share access */
|
||||
BOOLEAN HandleCleanup) /* TRUE to decrement handle count */
|
||||
|
@ -1441,7 +1441,8 @@ static NTSTATUS FspFsvolSetDispositionInformation(
|
||||
|
||||
NTSTATUS Result;
|
||||
PFILE_OBJECT FileObject = IrpSp->FileObject;
|
||||
PFILE_DISPOSITION_INFORMATION Info = (PFILE_DISPOSITION_INFORMATION)Irp->AssociatedIrp.SystemBuffer;
|
||||
FILE_INFORMATION_CLASS FileInformationClass = IrpSp->Parameters.SetFile.FileInformationClass;
|
||||
UINT32 DispositionFlags;
|
||||
ULONG Length = IrpSp->Parameters.SetFile.Length;
|
||||
FSP_FILE_NODE *FileNode = FileObject->FsContext;
|
||||
FSP_FILE_DESC *FileDesc = FileObject->FsContext2;
|
||||
@ -1449,9 +1450,34 @@ static NTSTATUS FspFsvolSetDispositionInformation(
|
||||
BOOLEAN Success;
|
||||
|
||||
ASSERT(FileNode == FileDesc->FileNode);
|
||||
ASSERT(
|
||||
FileDispositionInformation == FileInformationClass ||
|
||||
FileDispositionInformationEx == FileInformationClass);
|
||||
|
||||
if (FileDispositionInformation == FileInformationClass)
|
||||
{
|
||||
if (sizeof(FILE_DISPOSITION_INFORMATION) > Length)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
DispositionFlags = !!((PFILE_DISPOSITION_INFORMATION)Irp->AssociatedIrp.SystemBuffer)->DeleteFile;
|
||||
DispositionFlags |= FILE_DISPOSITION_FORCE_IMAGE_SECTION_CHECK;
|
||||
// old-school delete always did image section check; see below
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!FspFsvolDeviceExtension(FsvolDeviceObject)->VolumeParams.SupportsPosixUnlinkRename)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
if (sizeof(FILE_DISPOSITION_INFORMATION_EX) > Length)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
DispositionFlags = ((PFILE_DISPOSITION_INFORMATION_EX)Irp->AssociatedIrp.SystemBuffer)->Flags;
|
||||
|
||||
/* !!!: REVISIT:
|
||||
* For now we cannot handle the FILE_DISPOSITION_ON_CLOSE flag,
|
||||
* as we need to understand the semantics better.
|
||||
*/
|
||||
if (FlagOn(DispositionFlags, FILE_DISPOSITION_ON_CLOSE))
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
if (sizeof(FILE_DISPOSITION_INFORMATION) > Length)
|
||||
return STATUS_INVALID_PARAMETER;
|
||||
if (FileNode->IsRootDirectory)
|
||||
/* cannot delete root directory */
|
||||
return STATUS_CANNOT_DELETE;
|
||||
@ -1459,7 +1485,7 @@ static NTSTATUS FspFsvolSetDispositionInformation(
|
||||
retry:
|
||||
FspFileNodeAcquireExclusive(FileNode, Full);
|
||||
|
||||
if (Info->DeleteFile)
|
||||
if (FlagOn(DispositionFlags, FILE_DISPOSITION_DELETE))
|
||||
{
|
||||
/*
|
||||
* Perform oplock check.
|
||||
@ -1487,15 +1513,40 @@ retry:
|
||||
if (!NT_SUCCESS(Result))
|
||||
goto unlock_exit;
|
||||
|
||||
/* make sure no process is mapping the file as an image */
|
||||
Success = MmFlushImageSection(FileObject->SectionObjectPointer, MmFlushForDelete);
|
||||
if (!Success)
|
||||
if (FlagOn(DispositionFlags, FILE_DISPOSITION_FORCE_IMAGE_SECTION_CHECK))
|
||||
{
|
||||
Result = STATUS_CANNOT_DELETE;
|
||||
goto unlock_exit;
|
||||
/* make sure no process is mapping the file as an image */
|
||||
Success = MmFlushImageSection(FileObject->SectionObjectPointer, MmFlushForDelete);
|
||||
if (!Success)
|
||||
{
|
||||
Result = STATUS_CANNOT_DELETE;
|
||||
goto unlock_exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (FlagOn(DispositionFlags, FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE))
|
||||
{
|
||||
/* if FileDesc does not have FILE_WRITE_ATTRIBUTE access, remove IGNORE_READONLY_ATTRIBUTE */
|
||||
if (!FlagOn(FileDesc->GrantedAccess, FILE_WRITE_ATTRIBUTES))
|
||||
DispositionFlags &= ~FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE;
|
||||
}
|
||||
}
|
||||
|
||||
if (FileNode->PosixDelete)
|
||||
{
|
||||
Result = STATUS_SUCCESS;
|
||||
goto unlock_exit;
|
||||
}
|
||||
|
||||
if (FlagOn(DispositionFlags, FILE_DISPOSITION_DELETE))
|
||||
DispositionFlags &=
|
||||
FILE_DISPOSITION_DO_NOT_DELETE |
|
||||
FILE_DISPOSITION_DELETE |
|
||||
FILE_DISPOSITION_POSIX_SEMANTICS |
|
||||
FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE;
|
||||
else
|
||||
DispositionFlags = FILE_DISPOSITION_DO_NOT_DELETE;
|
||||
|
||||
Result = FspIopCreateRequestEx(Irp, &FileNode->FileName, 0,
|
||||
FspFsvolSetInformationRequestFini, &Request);
|
||||
if (!NT_SUCCESS(Result))
|
||||
@ -1504,8 +1555,8 @@ retry:
|
||||
Request->Kind = FspFsctlTransactSetInformationKind;
|
||||
Request->Req.SetInformation.UserContext = FileNode->UserContext;
|
||||
Request->Req.SetInformation.UserContext2 = FileDesc->UserContext2;
|
||||
Request->Req.SetInformation.FileInformationClass = FileDispositionInformation;
|
||||
Request->Req.SetInformation.Info.Disposition.Delete = Info->DeleteFile;
|
||||
Request->Req.SetInformation.FileInformationClass = FileInformationClass;
|
||||
Request->Req.SetInformation.Info.DispositionEx.Flags = DispositionFlags;
|
||||
|
||||
FspFileNodeSetOwner(FileNode, Full, Request);
|
||||
FspIopRequestContext(Request, RequestFileNode) = FileNode;
|
||||
@ -1525,23 +1576,51 @@ static NTSTATUS FspFsvolSetDispositionInformationSuccess(
|
||||
|
||||
PIO_STACK_LOCATION IrpSp = IoGetCurrentIrpStackLocation(Irp);
|
||||
PFILE_OBJECT FileObject = IrpSp->FileObject;
|
||||
PFILE_DISPOSITION_INFORMATION Info = (PFILE_DISPOSITION_INFORMATION)Irp->AssociatedIrp.SystemBuffer;
|
||||
FSP_FILE_NODE *FileNode = FileObject->FsContext;
|
||||
FSP_FSCTL_TRANSACT_REQ *Request = FspIrpRequest(Irp);
|
||||
UINT32 DispositionFlags = Request->Req.SetInformation.Info.DispositionEx.Flags;
|
||||
BOOLEAN DeleteFile = BooleanFlagOn(DispositionFlags, FILE_DISPOSITION_DELETE);
|
||||
|
||||
FileNode->DeletePending = Info->DeleteFile;
|
||||
FileObject->DeletePending = Info->DeleteFile;
|
||||
FileNode->DeletePending = DeleteFile;
|
||||
FileObject->DeletePending = DeleteFile;
|
||||
|
||||
/* fastfat does this, although it seems unnecessary */
|
||||
#if 1
|
||||
if (FileNode->IsDirectory && Info->DeleteFile)
|
||||
if (FlagOn(DispositionFlags, FILE_DISPOSITION_POSIX_SEMANTICS))
|
||||
{
|
||||
FSP_FSVOL_DEVICE_EXTENSION *FsvolDeviceExtension =
|
||||
FspFsvolDeviceExtension(IrpSp->DeviceObject);
|
||||
FspNotifyDeletePending(
|
||||
FsvolDeviceExtension->NotifySync, &FsvolDeviceExtension->NotifyList, FileNode);
|
||||
ASSERT(DeleteFile);
|
||||
|
||||
FileNode->PosixDelete = TRUE;
|
||||
|
||||
if (FileNode->IsDirectory)
|
||||
{
|
||||
FSP_FSVOL_DEVICE_EXTENSION *FsvolDeviceExtension =
|
||||
FspFsvolDeviceExtension(IrpSp->DeviceObject);
|
||||
FspNotifyDeletePending(
|
||||
FsvolDeviceExtension->NotifySync, &FsvolDeviceExtension->NotifyList, FileNode);
|
||||
}
|
||||
|
||||
/* send the appropriate notification; also invalidate dirinfo/etc. caches */
|
||||
ULONG NotifyFilter, NotifyAction;
|
||||
NotifyFilter = FileNode->IsDirectory ?
|
||||
FILE_NOTIFY_CHANGE_DIR_NAME : FILE_NOTIFY_CHANGE_FILE_NAME;
|
||||
NotifyAction = FILE_ACTION_REMOVED;
|
||||
FspFileNodeNotifyChange(FileNode, NotifyFilter, NotifyAction, TRUE);
|
||||
|
||||
/* perform POSIX delete: remove file node from the context table */
|
||||
FspFileNodePosixDelete(FileNode, FileObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* fastfat does this, although it seems unnecessary */
|
||||
#if 1
|
||||
if (FileNode->IsDirectory && DeleteFile)
|
||||
{
|
||||
FSP_FSVOL_DEVICE_EXTENSION *FsvolDeviceExtension =
|
||||
FspFsvolDeviceExtension(IrpSp->DeviceObject);
|
||||
FspNotifyDeletePending(
|
||||
FsvolDeviceExtension->NotifySync, &FsvolDeviceExtension->NotifyList, FileNode);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
FspIopRequestContext(Request, RequestFileNode) = 0;
|
||||
FspFileNodeReleaseOwner(FileNode, Full, Request);
|
||||
@ -1796,10 +1875,15 @@ static NTSTATUS FspFsvolSetInformation(
|
||||
FILE_INFORMATION_CLASS FileInformationClass = IrpSp->Parameters.SetFile.FileInformationClass;
|
||||
|
||||
/* special case FileDispositionInformation/FileRenameInformation */
|
||||
if (FileDispositionInformation == FileInformationClass)
|
||||
switch (FileInformationClass)
|
||||
{
|
||||
case FileDispositionInformation:
|
||||
case FileDispositionInformationEx:
|
||||
return FspFsvolSetDispositionInformation(FsvolDeviceObject, Irp, IrpSp);
|
||||
if (FileRenameInformation == FileInformationClass)
|
||||
case FileRenameInformation:
|
||||
//case FileRenameInformationEx:
|
||||
return FspFsvolSetRenameInformation(FsvolDeviceObject, Irp, IrpSp);
|
||||
}
|
||||
|
||||
NTSTATUS Result;
|
||||
PFILE_OBJECT FileObject = IrpSp->FileObject;
|
||||
@ -1997,10 +2081,15 @@ NTSTATUS FspFsvolSetInformationComplete(
|
||||
FILE_INFORMATION_CLASS FileInformationClass = IrpSp->Parameters.SetFile.FileInformationClass;
|
||||
|
||||
/* special case FileDispositionInformation/FileRenameInformation */
|
||||
if (FileDispositionInformation == FileInformationClass)
|
||||
switch (FileInformationClass)
|
||||
{
|
||||
case FileDispositionInformation:
|
||||
case FileDispositionInformationEx:
|
||||
FSP_RETURN(Result = FspFsvolSetDispositionInformationSuccess(Irp, Response));
|
||||
if (FileRenameInformation == FileInformationClass)
|
||||
case FileRenameInformation:
|
||||
//case FileRenameInformationEx:
|
||||
FSP_RETURN(Result = FspFsvolSetRenameInformationSuccess(Irp, Response));
|
||||
}
|
||||
|
||||
PFILE_OBJECT FileObject = IrpSp->FileObject;
|
||||
FSP_FILE_NODE *FileNode = FileObject->FsContext;
|
||||
|
@ -101,7 +101,8 @@ static NTSTATUS FspFsvolQueryFsAttributeInformation(
|
||||
(FsvolDeviceExtension->VolumeParams.NamedStreams ? FILE_NAMED_STREAMS : 0) |
|
||||
//(FsvolDeviceExtension->VolumeParams.HardLinks ? FILE_SUPPORTS_HARD_LINKS : 0) |
|
||||
(FsvolDeviceExtension->VolumeParams.ExtendedAttributes ? FILE_SUPPORTS_EXTENDED_ATTRIBUTES : 0) |
|
||||
(FsvolDeviceExtension->VolumeParams.ReadOnlyVolume ? FILE_READ_ONLY_VOLUME : 0);
|
||||
(FsvolDeviceExtension->VolumeParams.ReadOnlyVolume ? FILE_READ_ONLY_VOLUME : 0) |
|
||||
(FsvolDeviceExtension->VolumeParams.SupportsPosixUnlinkRename ? FILE_SUPPORTS_POSIX_UNLINK_RENAME : 0);
|
||||
Info->MaximumComponentNameLength = FsvolDeviceExtension->VolumeParams.MaxComponentLength;
|
||||
|
||||
RtlInitUnicodeString(&FileSystemName, FsvolDeviceExtension->VolumeParams.FileSystemName);
|
||||
|
Reference in New Issue
Block a user