Skip to content

Commit e2f3e53

Browse files
lheckerDHowett
andauthored
Don't trim bracketed pastes (#19067)
* Moves clipboard writing from `ControlCore` to `TerminalPage`. This requires adding a bunch of event types and logic. This is technically not needed anymore after changing the direction of this PR, but I kept it because it's better. * Add a `WarnAboutMultiLinePaste` enum to differentiate between "paste without warning always/never/if-bracketed-paste-disabled". Closes #13014 Closes microsoft/edit#279 ## Validation Steps Performed * Launch Microsoft Edit and copy text with a trailing newline * Paste it with Ctrl+Shift+V * It's pasted as it was copied ✅ * Changing the setting to "always" always warns ✅ Co-authored-by: Dustin L. Howett <[email protected]>
1 parent 5b41f14 commit e2f3e53

28 files changed

+305
-206
lines changed

src/cascadia/TerminalApp/AppActionHandlers.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,9 @@ namespace winrt::TerminalApp::implementation
548548
{
549549
if (const auto& realArgs = args.ActionArgs().try_as<CopyTextArgs>())
550550
{
551-
const auto handled = _CopyText(realArgs.DismissSelection(), realArgs.SingleLine(), realArgs.WithControlSequences(), realArgs.CopyFormatting());
551+
const auto copyFormatting = realArgs.CopyFormatting();
552+
const auto format = copyFormatting ? copyFormatting.Value() : _settings.GlobalSettings().CopyFormatting();
553+
const auto handled = _CopyText(realArgs.DismissSelection(), realArgs.SingleLine(), realArgs.WithControlSequences(), format);
552554
args.Handled(handled);
553555
}
554556
}

src/cascadia/TerminalApp/TerminalPage.cpp

Lines changed: 163 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,121 @@ namespace winrt
5656
using VirtualKeyModifiers = Windows::System::VirtualKeyModifiers;
5757
}
5858

59+
namespace clipboard
60+
{
61+
wil::unique_close_clipboard_call open(HWND hwnd)
62+
{
63+
bool success = false;
64+
65+
// OpenClipboard may fail to acquire the internal lock --> retry.
66+
for (DWORD sleep = 10;; sleep *= 2)
67+
{
68+
if (OpenClipboard(hwnd))
69+
{
70+
success = true;
71+
break;
72+
}
73+
// 10 iterations
74+
if (sleep > 10000)
75+
{
76+
break;
77+
}
78+
Sleep(sleep);
79+
}
80+
81+
return wil::unique_close_clipboard_call{ success };
82+
}
83+
84+
void write(wil::zwstring_view text, std::string_view html, std::string_view rtf)
85+
{
86+
static const auto regular = [](const UINT format, const void* src, const size_t bytes) {
87+
wil::unique_hglobal handle{ THROW_LAST_ERROR_IF_NULL(GlobalAlloc(GMEM_MOVEABLE, bytes)) };
88+
89+
const auto locked = GlobalLock(handle.get());
90+
memcpy(locked, src, bytes);
91+
GlobalUnlock(handle.get());
92+
93+
THROW_LAST_ERROR_IF_NULL(SetClipboardData(format, handle.get()));
94+
handle.release();
95+
};
96+
static const auto registered = [](const wchar_t* format, const void* src, size_t bytes) {
97+
const auto id = RegisterClipboardFormatW(format);
98+
if (!id)
99+
{
100+
LOG_LAST_ERROR();
101+
return;
102+
}
103+
regular(id, src, bytes);
104+
};
105+
106+
EmptyClipboard();
107+
108+
if (!text.empty())
109+
{
110+
// As per: https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
111+
// CF_UNICODETEXT: [...] A null character signals the end of the data.
112+
// --> We add +1 to the length. This works because .c_str() is null-terminated.
113+
regular(CF_UNICODETEXT, text.c_str(), (text.size() + 1) * sizeof(wchar_t));
114+
}
115+
116+
if (!html.empty())
117+
{
118+
registered(L"HTML Format", html.data(), html.size());
119+
}
120+
121+
if (!rtf.empty())
122+
{
123+
registered(L"Rich Text Format", rtf.data(), rtf.size());
124+
}
125+
}
126+
127+
winrt::hstring read()
128+
{
129+
// This handles most cases of pasting text as the OS converts most formats to CF_UNICODETEXT automatically.
130+
if (const auto handle = GetClipboardData(CF_UNICODETEXT))
131+
{
132+
const wil::unique_hglobal_locked lock{ handle };
133+
const auto str = static_cast<const wchar_t*>(lock.get());
134+
if (!str)
135+
{
136+
return {};
137+
}
138+
139+
const auto maxLen = GlobalSize(handle) / sizeof(wchar_t);
140+
const auto len = wcsnlen(str, maxLen);
141+
return winrt::hstring{ str, gsl::narrow_cast<uint32_t>(len) };
142+
}
143+
144+
// We get CF_HDROP when a user copied a file with Ctrl+C in Explorer and pastes that into the terminal (among others).
145+
if (const auto handle = GetClipboardData(CF_HDROP))
146+
{
147+
const wil::unique_hglobal_locked lock{ handle };
148+
const auto drop = static_cast<HDROP>(lock.get());
149+
if (!drop)
150+
{
151+
return {};
152+
}
153+
154+
const auto cap = DragQueryFileW(drop, 0, nullptr, 0);
155+
if (cap == 0)
156+
{
157+
return {};
158+
}
159+
160+
auto buffer = winrt::impl::hstring_builder{ cap };
161+
const auto len = DragQueryFileW(drop, 0, buffer.data(), cap + 1);
162+
if (len == 0)
163+
{
164+
return {};
165+
}
166+
167+
return buffer.to_hstring();
168+
}
169+
170+
return {};
171+
}
172+
} // namespace clipboard
173+
59174
namespace winrt::TerminalApp::implementation
60175
{
61176
TerminalPage::TerminalPage(TerminalApp::WindowProperties properties, const TerminalApp::ContentManager& manager) :
@@ -1793,7 +1908,7 @@ namespace winrt::TerminalApp::implementation
17931908
{
17941909
term.RaiseNotice({ this, &TerminalPage::_ControlNoticeRaisedHandler });
17951910

1796-
// Add an event handler when the terminal wants to paste data from the Clipboard.
1911+
term.WriteToClipboard({ get_weak(), &TerminalPage::_copyToClipboard });
17971912
term.PasteFromClipboard({ this, &TerminalPage::_PasteFromClipboardHandler });
17981913

17991914
term.OpenHyperlink({ this, &TerminalPage::_OpenHyperlinkHandler });
@@ -1815,9 +1930,7 @@ namespace winrt::TerminalApp::implementation
18151930
});
18161931

18171932
term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler });
1818-
18191933
term.SearchMissingCommand({ get_weak(), &TerminalPage::_SearchMissingCommandHandler });
1820-
18211934
term.WindowSizeChanged({ get_weak(), &TerminalPage::_WindowSizeChanged });
18221935

18231936
// Don't even register for the event if the feature is compiled off.
@@ -2739,75 +2852,6 @@ namespace winrt::TerminalApp::implementation
27392852
return dimension;
27402853
}
27412854

2742-
static wil::unique_close_clipboard_call _openClipboard(HWND hwnd)
2743-
{
2744-
bool success = false;
2745-
2746-
// OpenClipboard may fail to acquire the internal lock --> retry.
2747-
for (DWORD sleep = 10;; sleep *= 2)
2748-
{
2749-
if (OpenClipboard(hwnd))
2750-
{
2751-
success = true;
2752-
break;
2753-
}
2754-
// 10 iterations
2755-
if (sleep > 10000)
2756-
{
2757-
break;
2758-
}
2759-
Sleep(sleep);
2760-
}
2761-
2762-
return wil::unique_close_clipboard_call{ success };
2763-
}
2764-
2765-
static winrt::hstring _extractClipboard()
2766-
{
2767-
// This handles most cases of pasting text as the OS converts most formats to CF_UNICODETEXT automatically.
2768-
if (const auto handle = GetClipboardData(CF_UNICODETEXT))
2769-
{
2770-
const wil::unique_hglobal_locked lock{ handle };
2771-
const auto str = static_cast<const wchar_t*>(lock.get());
2772-
if (!str)
2773-
{
2774-
return {};
2775-
}
2776-
2777-
const auto maxLen = GlobalSize(handle) / sizeof(wchar_t);
2778-
const auto len = wcsnlen(str, maxLen);
2779-
return winrt::hstring{ str, gsl::narrow_cast<uint32_t>(len) };
2780-
}
2781-
2782-
// We get CF_HDROP when a user copied a file with Ctrl+C in Explorer and pastes that into the terminal (among others).
2783-
if (const auto handle = GetClipboardData(CF_HDROP))
2784-
{
2785-
const wil::unique_hglobal_locked lock{ handle };
2786-
const auto drop = static_cast<HDROP>(lock.get());
2787-
if (!drop)
2788-
{
2789-
return {};
2790-
}
2791-
2792-
const auto cap = DragQueryFileW(drop, 0, nullptr, 0);
2793-
if (cap == 0)
2794-
{
2795-
return {};
2796-
}
2797-
2798-
auto buffer = winrt::impl::hstring_builder{ cap };
2799-
const auto len = DragQueryFileW(drop, 0, buffer.data(), cap + 1);
2800-
if (len == 0)
2801-
{
2802-
return {};
2803-
}
2804-
2805-
return buffer.to_hstring();
2806-
}
2807-
2808-
return {};
2809-
}
2810-
28112855
// Function Description:
28122856
// - This function is called when the `TermControl` requests that we send
28132857
// it the clipboard's content.
@@ -2827,36 +2871,51 @@ namespace winrt::TerminalApp::implementation
28272871
const auto weakThis = get_weak();
28282872
const auto dispatcher = Dispatcher();
28292873
const auto globalSettings = _settings.GlobalSettings();
2874+
const auto bracketedPaste = eventArgs.BracketedPasteEnabled();
28302875

28312876
// GetClipboardData might block for up to 30s for delay-rendered contents.
28322877
co_await winrt::resume_background();
28332878

28342879
winrt::hstring text;
2835-
if (const auto clipboard = _openClipboard(nullptr))
2880+
if (const auto clipboard = clipboard::open(nullptr))
28362881
{
2837-
text = _extractClipboard();
2882+
text = clipboard::read();
28382883
}
28392884

2840-
if (globalSettings.TrimPaste())
2885+
if (!bracketedPaste && globalSettings.TrimPaste())
28412886
{
2842-
text = { Utils::TrimPaste(text) };
2843-
if (text.empty())
2844-
{
2845-
// Text is all white space, nothing to paste
2846-
co_return;
2847-
}
2887+
text = winrt::hstring{ Utils::TrimPaste(text) };
2888+
}
2889+
2890+
if (text.empty())
2891+
{
2892+
co_return;
2893+
}
2894+
2895+
bool warnMultiLine = false;
2896+
switch (globalSettings.WarnAboutMultiLinePaste())
2897+
{
2898+
case WarnAboutMultiLinePaste::Automatic:
2899+
// NOTE that this is unsafe, because a shell that doesn't support bracketed paste
2900+
// will allow an attacker to enable the mode, not realize that, and then accept
2901+
// the paste as if it was a series of legitimate commands. See GH#13014.
2902+
warnMultiLine = !bracketedPaste;
2903+
break;
2904+
case WarnAboutMultiLinePaste::Always:
2905+
warnMultiLine = true;
2906+
break;
2907+
default:
2908+
warnMultiLine = false;
2909+
break;
28482910
}
28492911

2850-
// If the requesting terminal is in bracketed paste mode, then we don't need to warn about a multi-line paste.
2851-
auto warnMultiLine = globalSettings.WarnAboutMultiLinePaste() && !eventArgs.BracketedPasteEnabled();
28522912
if (warnMultiLine)
28532913
{
2854-
const auto isNewLineLambda = [](auto c) { return c == L'\n' || c == L'\r'; };
2855-
const auto hasNewLine = std::find_if(text.cbegin(), text.cend(), isNewLineLambda) != text.cend();
2856-
warnMultiLine = hasNewLine;
2914+
const std::wstring_view view{ text };
2915+
warnMultiLine = view.find_first_of(L"\r\n") != std::wstring_view::npos;
28572916
}
28582917

2859-
constexpr const std::size_t minimumSizeForWarning = 1024 * 5; // 5 KiB
2918+
constexpr std::size_t minimumSizeForWarning = 1024 * 5; // 5 KiB
28602919
const auto warnLargeText = text.size() > minimumSizeForWarning && globalSettings.WarnAboutLargePaste();
28612920

28622921
if (warnMultiLine || warnLargeText)
@@ -2866,7 +2925,7 @@ namespace winrt::TerminalApp::implementation
28662925
if (const auto strongThis = weakThis.get())
28672926
{
28682927
// We have to initialize the dialog here to be able to change the text of the text block within it
2869-
FindName(L"MultiLinePasteDialog").try_as<WUX::Controls::ContentDialog>();
2928+
std::ignore = FindName(L"MultiLinePasteDialog");
28702929
ClipboardText().Text(text);
28712930

28722931
// The vertical offset on the scrollbar does not reset automatically, so reset it manually
@@ -3047,7 +3106,7 @@ namespace winrt::TerminalApp::implementation
30473106
// - formats: dictate which formats need to be copied
30483107
// Return Value:
30493108
// - true iff we we able to copy text (if a selection was active)
3050-
bool TerminalPage::_CopyText(const bool dismissSelection, const bool singleLine, const bool withControlSequences, const Windows::Foundation::IReference<CopyFormat>& formats)
3109+
bool TerminalPage::_CopyText(const bool dismissSelection, const bool singleLine, const bool withControlSequences, const CopyFormat formats)
30513110
{
30523111
if (const auto& control{ _GetActiveControl() })
30533112
{
@@ -3190,6 +3249,21 @@ namespace winrt::TerminalApp::implementation
31903249
}
31913250
}
31923251

3252+
void TerminalPage::_copyToClipboard(const IInspectable, const WriteToClipboardEventArgs args) const
3253+
{
3254+
if (const auto clipboard = clipboard::open(_hostingHwnd.value_or(nullptr)))
3255+
{
3256+
const auto plain = args.Plain();
3257+
const auto html = args.Html();
3258+
const auto rtf = args.Rtf();
3259+
3260+
clipboard::write(
3261+
{ plain.data(), plain.size() },
3262+
{ reinterpret_cast<const char*>(html.data()), html.size() },
3263+
{ reinterpret_cast<const char*>(rtf.data()), rtf.size() });
3264+
}
3265+
}
3266+
31933267
// Method Description:
31943268
// - Paste text from the Windows Clipboard to the focused terminal
31953269
void TerminalPage::_PasteText()

src/cascadia/TerminalApp/TerminalPage.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,10 +413,11 @@ namespace winrt::TerminalApp::implementation
413413
bool _IsUriSupported(const winrt::Windows::Foundation::Uri& parsedUri);
414414

415415
void _ShowCouldNotOpenDialog(winrt::hstring reason, winrt::hstring uri);
416-
bool _CopyText(const bool dismissSelection, const bool singleLine, const bool withControlSequences, const Windows::Foundation::IReference<Microsoft::Terminal::Control::CopyFormat>& formats);
416+
bool _CopyText(bool dismissSelection, bool singleLine, bool withControlSequences, Microsoft::Terminal::Control::CopyFormat formats);
417417

418418
safe_void_coroutine _SetTaskbarProgressHandler(const IInspectable sender, const IInspectable eventArgs);
419419

420+
void _copyToClipboard(IInspectable, Microsoft::Terminal::Control::WriteToClipboardEventArgs args) const;
420421
void _PasteText();
421422

422423
safe_void_coroutine _ControlNoticeRaisedHandler(const IInspectable sender, const Microsoft::Terminal::Control::NoticeEventArgs eventArgs);

0 commit comments

Comments
 (0)