mirror of
https://github.com/PowerShell/Win32-OpenSSH.git
synced 2025-07-05 05:04:47 +02:00
Manual Merge change 04148d0cea5f686d8669dda2fb8d3f42cedc2a47
ID Author Date Message 04148d0cea5f686d8669dda2fb8d3f42cedc2a47 Ray Hayes <rayhayes@rhbe.net> 9/20/2016 1:34:46 PM -07:00 Fix a scrolling issue on Windows Server.
This commit is contained in:
parent
57c6793fc0
commit
d0ea7fc5dc
@ -39,6 +39,14 @@
|
|||||||
#define WM_APPEXIT WM_USER+1
|
#define WM_APPEXIT WM_USER+1
|
||||||
#define MAX_EXPECTED_BUFFER_SIZE 1024
|
#define MAX_EXPECTED_BUFFER_SIZE 1024
|
||||||
|
|
||||||
|
#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||||
|
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x4
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef ENABLE_VIRTUAL_TERMINAL_INPUT
|
||||||
|
#define ENABLE_VIRTUAL_TERMINAL_INPUT 0x0200
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct consoleEvent {
|
typedef struct consoleEvent {
|
||||||
DWORD event;
|
DWORD event;
|
||||||
HWND hwnd;
|
HWND hwnd;
|
||||||
@ -56,6 +64,7 @@ BOOL istty = FALSE;
|
|||||||
BOOL bRet = FALSE;
|
BOOL bRet = FALSE;
|
||||||
BOOL bNoScrollRegion = FALSE;
|
BOOL bNoScrollRegion = FALSE;
|
||||||
BOOL bStartup = TRUE;
|
BOOL bStartup = TRUE;
|
||||||
|
BOOL bAnsi = FALSE;
|
||||||
|
|
||||||
HANDLE child_out = INVALID_HANDLE_VALUE;
|
HANDLE child_out = INVALID_HANDLE_VALUE;
|
||||||
HANDLE child_in = INVALID_HANDLE_VALUE;
|
HANDLE child_in = INVALID_HANDLE_VALUE;
|
||||||
@ -173,18 +182,11 @@ void SendSetCursor(HANDLE hInput, int X, int Y) {
|
|||||||
DWORD wr = 0;
|
DWORD wr = 0;
|
||||||
DWORD out = 0;
|
DWORD out = 0;
|
||||||
|
|
||||||
static int sLastX = 0;
|
|
||||||
static int sLastY = 0;
|
|
||||||
|
|
||||||
char formatted_output[255];
|
char formatted_output[255];
|
||||||
|
|
||||||
out = _snprintf_s(formatted_output, sizeof(formatted_output), _TRUNCATE, "\033[%d;%dH", Y, X);
|
out = _snprintf_s(formatted_output, sizeof(formatted_output), _TRUNCATE, "\033[%d;%dH", Y, X);
|
||||||
//if (X != sLastX || Y != sLastY)
|
if (bUseAnsiEmulation)
|
||||||
if (bUseAnsiEmulation)
|
WriteFile(hInput, formatted_output, out, &wr, NULL);
|
||||||
WriteFile(hInput, formatted_output, out, &wr, NULL);
|
|
||||||
|
|
||||||
sLastX = X;
|
|
||||||
sLastY = Y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SendVerticalScroll(HANDLE hInput, int lines) {
|
void SendVerticalScroll(HANDLE hInput, int lines) {
|
||||||
@ -225,6 +227,8 @@ void SendCharacter(HANDLE hInput, WORD attributes, char character) {
|
|||||||
|
|
||||||
char formatted_output[2048];
|
char formatted_output[2048];
|
||||||
|
|
||||||
|
static USHORT pColor = 0;
|
||||||
|
|
||||||
USHORT Color = 0;
|
USHORT Color = 0;
|
||||||
ULONG Status = 0;
|
ULONG Status = 0;
|
||||||
|
|
||||||
@ -301,14 +305,10 @@ void SendCharacter(HANDLE hInput, WORD attributes, char character) {
|
|||||||
|
|
||||||
StringCbPrintfExA(Next, SizeLeft, &Next, &SizeLeft, 0, "m", Color);
|
StringCbPrintfExA(Next, SizeLeft, &Next, &SizeLeft, 0, "m", Color);
|
||||||
|
|
||||||
if (bUseAnsiEmulation)
|
if (bUseAnsiEmulation && Color != pColor)
|
||||||
WriteFile(hInput, formatted_output, (Next - formatted_output), &wr, NULL);
|
WriteFile(hInput, formatted_output, (Next - formatted_output), &wr, NULL);
|
||||||
|
|
||||||
WriteFile(hInput, &character, 1, &wr, NULL);
|
WriteFile(hInput, &character, 1, &wr, NULL);
|
||||||
|
|
||||||
// Reset
|
|
||||||
if (bUseAnsiEmulation)
|
|
||||||
WriteFile(hInput, "\033[0m", 4, &wr, NULL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SendBuffer(HANDLE hInput, CHAR_INFO *buffer, DWORD bufferSize) {
|
void SendBuffer(HANDLE hInput, CHAR_INFO *buffer, DWORD bufferSize) {
|
||||||
@ -357,24 +357,7 @@ void SendBuffer(HANDLE hInput, CHAR_INFO *buffer, DWORD bufferSize) {
|
|||||||
|
|
||||||
void CalculateAndSetCursor(HANDLE hInput, UINT aboveTopLine, UINT viewPortHeight, UINT x, UINT y) {
|
void CalculateAndSetCursor(HANDLE hInput, UINT aboveTopLine, UINT viewPortHeight, UINT x, UINT y) {
|
||||||
|
|
||||||
if (aboveTopLine > 0) {
|
SendSetCursor(pipe_out, x + 1, y + 1);
|
||||||
if (y == viewPortHeight + aboveTopLine - 1) {
|
|
||||||
if (ViewPortY == lastViewPortY && lastViewPortY > 0) {
|
|
||||||
SendSetCursor(pipe_out, x + 1, y + 1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SendSetCursor(pipe_out, x + 1, y + 1);
|
|
||||||
}
|
|
||||||
consoleInfo = nextConsoleInfo;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SendSetCursor(pipe_out, x + 1, y + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SendSetCursor(pipe_out, x + 1, y + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentLine = y;
|
currentLine = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -438,71 +421,6 @@ void SizeWindow(HANDLE hInput) {
|
|||||||
bSuccess = GetConsoleScreenBufferInfoEx(child_out, &consoleInfo);
|
bSuccess = GetConsoleScreenBufferInfoEx(child_out, &consoleInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RepaintWindow(HANDLE hInput) {
|
|
||||||
|
|
||||||
SMALL_RECT readRect;
|
|
||||||
DWORD bufferSize = 0;
|
|
||||||
|
|
||||||
CONSOLE_SCREEN_BUFFER_INFOEX consoleInfo;
|
|
||||||
CHAR_INFO *pBuffer = NULL;
|
|
||||||
|
|
||||||
SendHideCursor(hInput);
|
|
||||||
|
|
||||||
ZeroMemory(&consoleInfo, sizeof(consoleInfo));
|
|
||||||
consoleInfo.cbSize = sizeof(consoleInfo);
|
|
||||||
|
|
||||||
if (GetConsoleScreenBufferInfoEx(child_out, &consoleInfo)) {
|
|
||||||
|
|
||||||
// Compute buffer size for the full window
|
|
||||||
bufferSize = (consoleInfo.srWindow.Bottom - consoleInfo.srWindow.Top + 1) *
|
|
||||||
(consoleInfo.srWindow.Right - consoleInfo.srWindow.Left + 1);
|
|
||||||
|
|
||||||
// Create the screen scrape buffer
|
|
||||||
pBuffer = (PCHAR_INFO)malloc(sizeof(CHAR_INFO) * bufferSize);
|
|
||||||
|
|
||||||
if (!pBuffer) {
|
|
||||||
goto final_processing;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out the buffer size
|
|
||||||
COORD coordBufSize;
|
|
||||||
coordBufSize.Y = (consoleInfo.srWindow.Bottom - consoleInfo.srWindow.Top + 1);
|
|
||||||
coordBufSize.X = (consoleInfo.srWindow.Right - consoleInfo.srWindow.Left + 1);
|
|
||||||
|
|
||||||
if (coordBufSize.X < 0 || coordBufSize.X > MAX_CONSOLE_COLUMNS ||
|
|
||||||
coordBufSize.Y < 0 || coordBufSize.Y > MAX_CONSOLE_ROWS)
|
|
||||||
{
|
|
||||||
goto final_processing;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The top left destination cell of the temporary buffer is row 0, col 0.
|
|
||||||
COORD coordBufCoord;
|
|
||||||
coordBufCoord.X = 0;
|
|
||||||
coordBufCoord.Y = 0;
|
|
||||||
|
|
||||||
// Copy the block from the screen buffer to the temporary buffer.
|
|
||||||
if (!ReadConsoleOutput(child_out, pBuffer, coordBufSize, coordBufCoord, &consoleInfo.srWindow))
|
|
||||||
{
|
|
||||||
goto final_processing;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set cursor location based on the reported location from the message.
|
|
||||||
CalculateAndSetCursor(pipe_out, 0, 0, 0, 0);
|
|
||||||
|
|
||||||
// Send the entire block.
|
|
||||||
SendBuffer(pipe_out, pBuffer, bufferSize);
|
|
||||||
|
|
||||||
free(pBuffer);
|
|
||||||
pBuffer = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
final_processing:
|
|
||||||
if (pBuffer)
|
|
||||||
free(pBuffer);
|
|
||||||
|
|
||||||
SendShowCursor(hInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
// End of VT output routines
|
// End of VT output routines
|
||||||
|
|
||||||
DWORD WINAPI MonitorChild(_In_ LPVOID lpParameter) {
|
DWORD WINAPI MonitorChild(_In_ LPVOID lpParameter) {
|
||||||
@ -709,13 +627,6 @@ DWORD ProcessEvent(void *p) {
|
|||||||
ViewPortY += vn;
|
ViewPortY += vn;
|
||||||
}
|
}
|
||||||
|
|
||||||
//SendVerticalScroll(pipe_out, vd);
|
|
||||||
|
|
||||||
//SendHorizontalScroll(pipe_out, hd);
|
|
||||||
|
|
||||||
if(vd < 0)
|
|
||||||
SendCRLF(pipe_out);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EVENT_CONSOLE_LAYOUT:
|
case EVENT_CONSOLE_LAYOUT:
|
||||||
@ -759,7 +670,7 @@ DWORD ProcessEvent(void *p) {
|
|||||||
return ERROR_SUCCESS;
|
return ERROR_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
DWORD ProcessEventQueue(void *p) {
|
DWORD WINAPI ProcessEventQueue(LPVOID p) {
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
|
|
||||||
@ -791,8 +702,23 @@ DWORD ProcessEventQueue(void *p) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child_out != INVALID_HANDLE_VALUE && child_out != NULL)
|
if (child_in != INVALID_HANDLE_VALUE && child_in != NULL &&
|
||||||
|
child_out != INVALID_HANDLE_VALUE && child_out != NULL)
|
||||||
{
|
{
|
||||||
|
DWORD dwInputMode;
|
||||||
|
DWORD dwOutputMode;
|
||||||
|
|
||||||
|
if (GetConsoleMode(child_in, &dwInputMode) && GetConsoleMode(child_out, &dwOutputMode)) {
|
||||||
|
if (((dwOutputMode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == ENABLE_VIRTUAL_TERMINAL_PROCESSING) &&
|
||||||
|
((dwInputMode & ENABLE_VIRTUAL_TERMINAL_INPUT) == ENABLE_VIRTUAL_TERMINAL_INPUT))
|
||||||
|
{
|
||||||
|
bAnsi = TRUE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
bAnsi = FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ZeroMemory(&consoleInfo, sizeof(consoleInfo));
|
ZeroMemory(&consoleInfo, sizeof(consoleInfo));
|
||||||
consoleInfo.cbSize = sizeof(consoleInfo);
|
consoleInfo.cbSize = sizeof(consoleInfo);
|
||||||
|
|
||||||
@ -860,7 +786,7 @@ void QueueEvent(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DWORD ProcessPipes(void *p) {
|
DWORD WINAPI ProcessPipes(LPVOID p) {
|
||||||
|
|
||||||
BOOL ret;
|
BOOL ret;
|
||||||
DWORD dwStatus;
|
DWORD dwStatus;
|
||||||
@ -869,8 +795,8 @@ DWORD ProcessPipes(void *p) {
|
|||||||
while (1) {
|
while (1) {
|
||||||
char buf[128];
|
char buf[128];
|
||||||
DWORD rd = 0, wr = 0, i = 0;
|
DWORD rd = 0, wr = 0, i = 0;
|
||||||
GOTO_CLEANUP_ON_FALSE(ReadFile(pipe_in, buf, 128, &rd, NULL));
|
|
||||||
|
|
||||||
|
GOTO_CLEANUP_ON_FALSE(ReadFile(pipe_in, buf, 128, &rd, NULL));
|
||||||
if (!istty) { /* no tty, just send it accross */
|
if (!istty) { /* no tty, just send it accross */
|
||||||
GOTO_CLEANUP_ON_FALSE(WriteFile(child_pipe_write, buf, rd, &wr, NULL));
|
GOTO_CLEANUP_ON_FALSE(WriteFile(child_pipe_write, buf, rd, &wr, NULL));
|
||||||
continue;
|
continue;
|
||||||
@ -882,95 +808,7 @@ DWORD ProcessPipes(void *p) {
|
|||||||
|
|
||||||
INPUT_RECORD ir;
|
INPUT_RECORD ir;
|
||||||
|
|
||||||
if (buf[i] == '\r')
|
if (bAnsi) {
|
||||||
{
|
|
||||||
SendKeyStroke(child_in, VK_RETURN, buf[0]);
|
|
||||||
}
|
|
||||||
else if (buf[i] == '\b')
|
|
||||||
{
|
|
||||||
SendKeyStroke(child_in, VK_BACK, buf[0]);
|
|
||||||
}
|
|
||||||
else if (buf[i] == '\t')
|
|
||||||
{
|
|
||||||
SendKeyStroke(child_in, VK_TAB, buf[0]);
|
|
||||||
}
|
|
||||||
else if (buf[i] == '\x1b')
|
|
||||||
{
|
|
||||||
// These are incoming ANSI keystrokes.
|
|
||||||
switch (rd) {
|
|
||||||
case 1:
|
|
||||||
SendKeyStroke(child_in, VK_ESCAPE, buf[0]);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
switch (buf[i + 1])
|
|
||||||
{
|
|
||||||
case '[':
|
|
||||||
switch (buf[i + 2])
|
|
||||||
{
|
|
||||||
case 'A':
|
|
||||||
SendKeyStroke(child_in, VK_UP, 0);
|
|
||||||
i = i + 2;
|
|
||||||
break;
|
|
||||||
case 'B':
|
|
||||||
SendKeyStroke(child_in, VK_DOWN, 0);
|
|
||||||
i = i + 2;
|
|
||||||
break;
|
|
||||||
case 'C':
|
|
||||||
SendKeyStroke(child_in, VK_RIGHT, 0);
|
|
||||||
i = i + 2;
|
|
||||||
break;
|
|
||||||
case 'D':
|
|
||||||
SendKeyStroke(child_in, VK_LEFT, 0);
|
|
||||||
i = i + 2;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 4:
|
|
||||||
switch (buf[i + 1]) {
|
|
||||||
case '[':
|
|
||||||
{
|
|
||||||
switch (buf[i + 2]) {
|
|
||||||
case '2':
|
|
||||||
switch (buf[i + 3]) {
|
|
||||||
case '~':
|
|
||||||
{
|
|
||||||
SendKeyStroke(child_in, VK_INSERT, 0);
|
|
||||||
i = i + 3;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '3':
|
|
||||||
switch (buf[i + 3]) {
|
|
||||||
case '~':
|
|
||||||
{
|
|
||||||
SendKeyStroke(child_in, VK_DELETE, 0);
|
|
||||||
i = i + 3;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// Write the string to the console
|
|
||||||
WriteConsole(child_in, buf, rd, &wr, NULL);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ir.EventType = KEY_EVENT;
|
ir.EventType = KEY_EVENT;
|
||||||
ir.Event.KeyEvent.bKeyDown = TRUE;
|
ir.Event.KeyEvent.bKeyDown = TRUE;
|
||||||
ir.Event.KeyEvent.wRepeatCount = 1;
|
ir.Event.KeyEvent.wRepeatCount = 1;
|
||||||
@ -983,6 +821,121 @@ DWORD ProcessPipes(void *p) {
|
|||||||
ir.Event.KeyEvent.bKeyDown = FALSE;
|
ir.Event.KeyEvent.bKeyDown = FALSE;
|
||||||
WriteConsoleInputA(child_in, &ir, 1, &wr);
|
WriteConsoleInputA(child_in, &ir, 1, &wr);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (buf[i] == '\r')
|
||||||
|
{
|
||||||
|
SendKeyStroke(child_in, VK_RETURN, buf[0]);
|
||||||
|
}
|
||||||
|
else if (buf[i] == '\b')
|
||||||
|
{
|
||||||
|
SendKeyStroke(child_in, VK_BACK, buf[0]);
|
||||||
|
}
|
||||||
|
else if (buf[i] == '\t')
|
||||||
|
{
|
||||||
|
SendKeyStroke(child_in, VK_TAB, buf[0]);
|
||||||
|
}
|
||||||
|
else if (buf[i] == '\x1b')
|
||||||
|
{
|
||||||
|
switch (rd) {
|
||||||
|
case 1:
|
||||||
|
SendKeyStroke(child_in, VK_ESCAPE, buf[0]);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
switch (buf[i + 1])
|
||||||
|
{
|
||||||
|
case '[':
|
||||||
|
switch (buf[i + 2])
|
||||||
|
{
|
||||||
|
case 'A':
|
||||||
|
SendKeyStroke(child_in, VK_UP, 0);
|
||||||
|
i = i + 2;
|
||||||
|
break;
|
||||||
|
case 'B':
|
||||||
|
SendKeyStroke(child_in, VK_DOWN, 0);
|
||||||
|
i = i + 2;
|
||||||
|
break;
|
||||||
|
case 'C':
|
||||||
|
SendKeyStroke(child_in, VK_RIGHT, 0);
|
||||||
|
i = i + 2;
|
||||||
|
break;
|
||||||
|
case 'D':
|
||||||
|
SendKeyStroke(child_in, VK_LEFT, 0);
|
||||||
|
i = i + 2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
switch (buf[i + 1]) {
|
||||||
|
case '[':
|
||||||
|
{
|
||||||
|
switch (buf[i + 2]) {
|
||||||
|
case '2':
|
||||||
|
switch (buf[i + 3]) {
|
||||||
|
case '~':
|
||||||
|
{
|
||||||
|
SendKeyStroke(child_in, VK_INSERT, 0);
|
||||||
|
i = i + 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '3':
|
||||||
|
switch (buf[i + 3]) {
|
||||||
|
case '~':
|
||||||
|
{
|
||||||
|
SendKeyStroke(child_in, VK_DELETE, 0);
|
||||||
|
i = i + 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
ir.EventType = KEY_EVENT;
|
||||||
|
ir.Event.KeyEvent.bKeyDown = TRUE;
|
||||||
|
ir.Event.KeyEvent.wRepeatCount = 1;
|
||||||
|
ir.Event.KeyEvent.wVirtualKeyCode = 0;
|
||||||
|
ir.Event.KeyEvent.wVirtualScanCode = 0;
|
||||||
|
ir.Event.KeyEvent.uChar.AsciiChar = buf[i];
|
||||||
|
ir.Event.KeyEvent.dwControlKeyState = 0;
|
||||||
|
WriteConsoleInputA(child_in, &ir, 1, &wr);
|
||||||
|
|
||||||
|
ir.Event.KeyEvent.bKeyDown = FALSE;
|
||||||
|
WriteConsoleInputA(child_in, &ir, 1, &wr);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ir.EventType = KEY_EVENT;
|
||||||
|
ir.Event.KeyEvent.bKeyDown = TRUE;
|
||||||
|
ir.Event.KeyEvent.wRepeatCount = 1;
|
||||||
|
ir.Event.KeyEvent.wVirtualKeyCode = 0;
|
||||||
|
ir.Event.KeyEvent.wVirtualScanCode = 0;
|
||||||
|
ir.Event.KeyEvent.uChar.AsciiChar = buf[i];
|
||||||
|
ir.Event.KeyEvent.dwControlKeyState = 0;
|
||||||
|
WriteConsoleInputA(child_in, &ir, 1, &wr);
|
||||||
|
|
||||||
|
ir.Event.KeyEvent.bKeyDown = FALSE;
|
||||||
|
WriteConsoleInputA(child_in, &ir, 1, &wr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user