-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathclipboard.cpp
489 lines (451 loc) · 22.4 KB
/
clipboard.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
/*
AutoHotkey
Copyright 2003-2008 Chris Mallett (support@autohotkey.com)
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
*/
#include "stdafx.h" // pre-compiled headers
#include "clipboard.h"
#include "globaldata.h" // for g_script.ScriptError() and g_ClipboardTimeout
#include "application.h" // for MsgSleep()
#include "util.h" // for strlcpy()
size_t Clipboard::Get(char *aBuf)
// If aBuf is NULL, it returns the length of the text on the clipboard and leaves the
// clipboard open. Otherwise, it copies the clipboard text into aBuf and closes
// the clipboard (UPDATE: But only if the clipboard is still open from a prior call
// to determine the length -- see later comments for details). In both cases, the
// length of the clipboard text is returned (or the value CLIPBOARD_FAILURE if error).
// If the clipboard is still open when the next MsgSleep() is called -- presumably
// because the caller never followed up with a second call to this function, perhaps
// due to having insufficient memory -- MsgSleep() will close it so that our
// app doesn't keep the clipboard tied up. Note: In all current cases, the caller
// will use MsgBox to display an error, which in turn calls MsgSleep(), which will
// immediately close the clipboard.
{
/*
// Seems best to always have done this even if we return early due to failure:
if (aBuf)
// It should be safe to do this even at its peak capacity, because caller
// would have then given us the last char in the buffer, which is already
// a zero terminator, so this would have no effect:
*aBuf = '\0';
UINT i, file_count = 0;
BOOL clipboard_contains_text = IsClipboardFormatAvailable(CF_TEXT);
BOOL clipboard_contains_files = IsClipboardFormatAvailable(CF_HDROP);
if (!(clipboard_contains_text || clipboard_contains_files))
return 0;
if (!mIsOpen)
{
// As a precaution, don't give the caller anything from the clipboard
// if the clipboard isn't already open from the caller's previous
// call to determine the size of what's on the clipboard (no other app
// can alter its size while we have it open). The is to prevent a
// buffer overflow from happening in a scenario such as the following:
// Caller calls us and we return zero size, either because there's no
// CF_TEXT on the clipboard orthere was a problem opening the clipboard.
// In these two cases, the clipboard isn't open, so by the time the
// caller calls us again, there's a chance (vanishingly small perhaps)
// that another app (if our thread were preempted long enough, or the
// platform is multiprocessor) will have changed the contents of the
// clipboard to something larger than zero. Thus, if we copy that
// into the caller's buffer, the buffer might overflow:
if (aBuf)
return 0;
if (!Open())
{
// Since this should be very rare, a shorter message is now used. Formerly, it was
// "Could not open clipboard for reading after many timed attempts. Another program is probably holding it open."
Close(CANT_OPEN_CLIPBOARD_READ);
return CLIPBOARD_FAILURE;
}
if ( !(mClipMemNow = g_clip.GetClipboardDataTimeout(clipboard_contains_files ? CF_HDROP : CF_TEXT)) )
{
// v1.0.47.04: Commented out the following that had been in effect when clipboard_contains_files==false:
// Close("GetClipboardData"); // Short error message since so rare.
// return CLIPBOARD_FAILURE;
// This was done because there are situations when GetClipboardData can fail indefinitely.
// For example, in Firefox, pulling down the Bookmarks menu then right-clicking "Bookmarks Toolbar
// Folder" then selecting "Copy" puts one or more formats on the clipboard that cause this problem.
// For details, search the forum for TYMED_NULL.
//
// v1.0.42.03: For the fix below, GetClipboardDataTimeout() knows not to try more than once
// for CF_HDROP.
// Fix for v1.0.31.02: When clipboard_contains_files==true, tolerate failure, which happens
// as a normal/expected outcome when there are files on the clipboard but either:
// 1) zero of them;
// 2) the CF_HDROP on the clipboard is somehow misformatted.
// If you select the parent ".." folder in WinRar then use the following hotkey, the script
// would previously yield a runtime error:
//#q::
//Send, ^c
//ClipWait, 0.5, 1
//msgbox %Clipboard%
//Return
Close();
if (aBuf)
*aBuf = '\0';
return 0;
}
// Although GlobalSize(mClipMemNow) can yield zero in some cases -- in which case GlobalLock() should
// not be attempted -- it probably can't yield zero for CF_HDROP and CF_TEXT because such a thing has
// never been reported by anyone. Therefore, GlobalSize() is currently not called.
if ( !(mClipMemNowLocked = (char *)GlobalLock(mClipMemNow)) )
{
Close("GlobalLock"); // Short error message since so rare.
return CLIPBOARD_FAILURE;
}
// Otherwise: Update length after every successful new open&lock:
// Determine the length (size - 1) of the buffer than would be
// needed to hold what's on the clipboard:
if (clipboard_contains_files)
{
if (file_count = DragQueryFile((HDROP)mClipMemNowLocked, 0xFFFFFFFF, "", 0))
{
mLength = (file_count - 1) * 2; // Init; -1 if don't want a newline after last file.
for (i = 0; i < file_count; ++i)
mLength += DragQueryFile((HDROP)mClipMemNowLocked, i, NULL, 0);
}
else
mLength = 0;
}
else // clipboard_contains_text
mLength = strlen(mClipMemNowLocked);
if (mLength >= CLIPBOARD_FAILURE) // Can't realistically happen, so just indicate silent failure.
return CLIPBOARD_FAILURE;
}
if (!aBuf)
return mLength;
// Above: Just return the length; don't close the clipboard because we expect
// to be called again soon. If for some reason we aren't called, MsgSleep()
// will automatically close the clipboard and clean up things. It's done this
// way to avoid the chance that the clipboard contents (and thus its length)
// will change while we don't have it open, possibly resulting in a buffer
// overflow. In addition, this approach performs better because it avoids
// the overhead of having to close and reopen the clipboard.
// Otherwise:
if (clipboard_contains_files)
{
if (file_count = DragQueryFile((HDROP)mClipMemNowLocked, 0xFFFFFFFF, "", 0))
for (i = 0; i < file_count; ++i)
{
// Caller has already ensured aBuf is large enough to hold them all:
aBuf += DragQueryFile((HDROP)mClipMemNowLocked, i, aBuf, 999);
if (i < file_count - 1) // i.e. don't add newline after the last filename.
{
*aBuf++ = '\r'; // These two are the proper newline sequence that the OS prefers.
*aBuf++ = '\n';
}
//else DragQueryFile() has ensured that aBuf is terminated.
}
// else aBuf has already been terminated upon entrance to this function.
}
else
strcpy(aBuf, mClipMemNowLocked); // Caller has already ensured that aBuf is large enough.
// Fix for v1.0.37: Close() is no longer called here because it prevents the clipboard variable
// from being referred to more than once in a line. For example:
// Msgbox %Clipboard%%Clipboard%
// ToolTip % StrLen(Clipboard) . Clipboard
// Instead, the clipboard is later closed in other places (search on CLOSE_CLIPBOARD_IF_OPEN
// to find them). The alternative to fixing it this way would be to let it reopen the clipboard
// by means getting rid of the following lines above:
//if (aBuf)
// return 0;
// However, that has the risks described in the comments above those two lines.
return mLength;
*/
}
ResultType Clipboard::Set(char *aBuf, UINT aLength) //, bool aTrimIt)
// Returns OK or FAIL.
{
/*
// It was already open for writing from a prior call. Return failure because callers that do this
// are probably handling things wrong:
if (IsReadyForWrite()) return FAIL;
if (!aBuf)
{
aBuf = "";
aLength = 0;
}
else
if (aLength == UINT_MAX) // Caller wants us to determine the length.
aLength = (UINT)strlen(aBuf);
if (aLength)
{
if (!PrepareForWrite(aLength + 1))
return FAIL; // It already displayed the error.
strlcpy(mClipMemNewLocked, aBuf, aLength + 1); // Copy only a substring, if aLength specifies such.
// Only trim when the caller told us to, rather than always if g_script.mIsAutoIt2
// is true, since AutoIt2 doesn't always trim things (e.g. FileReadLine probably
// does not trim the line that was read into its output var). UPDATE: This is
// no longer needed because I think AutoIt2 only auto-trims when SetEnv is used:
//if (aTrimIt)
// trim(mClipMemNewLocked);
}
// else just do the below to empty the clipboard, which is different than setting
// the clipboard equal to the empty string: it's not truly empty then, as reported
// by IsClipboardFormatAvailable(CF_TEXT) -- and we want to be able to make it truly
// empty for use with functions such as ClipWait:
return Commit(); // It will display any errors.
*/
}
char *Clipboard::PrepareForWrite(size_t aAllocSize)
{
/*
if (!aAllocSize) return NULL; // Caller should ensure that size is at least 1, i.e. room for the zero terminator.
if (IsReadyForWrite())
// It was already prepared due to a prior call. Currently, the most useful thing to do
// here is return the memory area that's already been reserved:
return mClipMemNewLocked;
// Note: I think GMEM_DDESHARE is recommended in addition to the usual GMEM_MOVEABLE:
// UPDATE: MSDN: "The following values are obsolete, but are provided for compatibility
// with 16-bit Windows. They are ignored.": GMEM_DDESHARE
if ( !(mClipMemNew = GlobalAlloc(GMEM_MOVEABLE, aAllocSize)) )
{
g_script.ScriptError("GlobalAlloc"); // Short error message since so rare.
return NULL;
}
if ( !(mClipMemNewLocked = (char *)GlobalLock(mClipMemNew)) )
{
mClipMemNew = GlobalFree(mClipMemNew); // This keeps mClipMemNew in sync with its state.
g_script.ScriptError("GlobalLock"); // Short error message since so rare.
return NULL;
}
mCapacity = (UINT)aAllocSize; // Keep mCapacity in sync with the state of mClipMemNewLocked.
*mClipMemNewLocked = '\0'; // Init for caller.
return mClipMemNewLocked; // The caller can now write to this mem.
*/
}
ResultType Clipboard::Commit(UINT aFormat)
// If this is called while mClipMemNew is NULL, the clipboard will be set to be truly
// empty, which is different from writing an empty string to it. Note: If the clipboard
// was already physically open, this function will close it as part of the commit (since
// whoever had it open before can't use the prior contents, since they're invalid).
{
/*
if (!mIsOpen && !Open())
// Since this should be very rare, a shorter message is now used. Formerly, it was
// "Could not open clipboard for writing after many timed attempts. Another program is probably holding it open."
return AbortWrite(CANT_OPEN_CLIPBOARD_WRITE);
if (!EmptyClipboard())
{
Close();
return AbortWrite("EmptyClipboard"); // Short error message since so rare.
}
if (mClipMemNew)
{
bool new_is_empty = false;
// Unlock prior to calling SetClipboardData:
if (mClipMemNewLocked) // probably always true if we're here.
{
// Best to access the memory while it's still locked, which is why this temp var is used:
// v1.0.40.02: The following was fixed to properly recognize 0x0000 as the Unicode string terminator,
// which fixes problems with Transform Unicode.
new_is_empty = !mClipMemNewLocked[0] && (aFormat != CF_UNICODETEXT || !mClipMemNewLocked[1]);
GlobalUnlock(mClipMemNew); // mClipMemNew not mClipMemNewLocked.
mClipMemNewLocked = NULL; // Keep this in sync with the above action.
mCapacity = 0; // Keep mCapacity in sync with the state of mClipMemNewLocked.
}
if (new_is_empty)
// Leave the clipboard truly empty rather than setting it to be the
// empty string (i.e. these two conditions are NOT the same).
// But be sure to free the memory since we're not giving custody
// of it to the system:
mClipMemNew = GlobalFree(mClipMemNew);
else
if (SetClipboardData(aFormat, mClipMemNew))
// In any of the failure conditions above, Close() ensures that mClipMemNew is
// freed if it was allocated. But now that we're here, the memory should not be
// freed because it is owned by the clipboard (it will free it at the appropriate time).
// Thus, we relinquish the memory because we shouldn't be looking at it anymore:
mClipMemNew = NULL;
else
{
Close();
return AbortWrite("SetClipboardData"); // Short error message since so rare.
}
}
// else we will close it after having done only the EmptyClipboard(), above.
// Note: Decided not to update mLength for performance reasons (in case clipboard is huge).
// Anyway, it seems rather pointless because once the clipboard is closed, our app instantly
// loses sight of how large it is, so the the value of mLength wouldn't be reliable unless
// the clipboard were going to be immediately opened again.
return Close();
*/
}
ResultType Clipboard::AbortWrite(char *aErrorMessage)
// Always returns FAIL.
{
/*
// Since we were called in conjunction with an aborted attempt to Commit(), always
// ensure the clipboard is physically closed because even an attempt to Commit()
// should physically close it:
Close();
if (mClipMemNewLocked)
{
GlobalUnlock(mClipMemNew); // mClipMemNew not mClipMemNewLocked.
mClipMemNewLocked = NULL;
mCapacity = 0; // Keep mCapacity in sync with the state of mClipMemNewLocked.
}
// Above: Unlock prior to freeing below.
if (mClipMemNew)
mClipMemNew = GlobalFree(mClipMemNew);
// Caller needs us to always return FAIL:
return *aErrorMessage ? g_script.ScriptError(aErrorMessage) : FAIL;
*/
}
ResultType Clipboard::Close(char *aErrorMessage)
// Returns OK or FAIL (but it only returns FAIL if caller gave us a non-NULL aErrorMessage).
{
/*
// Always close it ASAP so that it is free for other apps to use:
if (mIsOpen)
{
if (mClipMemNowLocked)
{
GlobalUnlock(mClipMemNow); // mClipMemNow not mClipMemNowLocked.
mClipMemNowLocked = NULL; // Keep this in sync with its state, since it's used as an indicator.
}
// Above: It's probably best to unlock prior to closing the clipboard.
CloseClipboard();
mIsOpen = false; // Even if above fails (realistically impossible?), seems best to do this.
// Must do this only after GlobalUnlock():
mClipMemNow = NULL;
}
// Do this cleanup for callers that didn't make it far enough to even open the clipboard.
// UPDATE: DO *NOT* do this because it is valid to have the clipboard in a "ReadyForWrite"
// state even after we physically close it. Some callers rely on that.
//if (mClipMemNewLocked)
//{
// GlobalUnlock(mClipMemNew); // mClipMemNew not mClipMemNewLocked.
// mClipMemNewLocked = NULL;
// mCapacity = 0; // Keep mCapacity in sync with the state of mClipMemNewLocked.
//}
//// Above: Unlock prior to freeing below.
//if (mClipMemNew)
// // Commit() was never called after a call to PrepareForWrite(), so just free the memory:
// mClipMemNew = GlobalFree(mClipMemNew);
if (aErrorMessage && *aErrorMessage)
// Caller needs us to always return FAIL if an error was displayed:
return g_script.ScriptError(aErrorMessage);
// Seems best not to reset mLength. But it will quickly become out of date once
// the clipboard has been closed and other apps can use it.
return OK;
*/
}
HANDLE Clipboard::GetClipboardDataTimeout(UINT uFormat)
// Same as GetClipboardData() except that it doesn't give up if the first call to GetClipboardData() fails.
// Instead, it continues to retry the operation for the number of milliseconds in g_ClipboardTimeout.
// This is necessary because GetClipboardData() has been observed to fail in repeatable situations (this
// is strange because our thread already has the clipboard locked open -- presumably it happens because the
// GetClipboardData() is unable to start a data stream from the application that actually serves up the data).
// If cases where the first call to GetClipboardData() fails, a subsequent call will often succeed if you give
// the owning application (such as Excel and Word) a little time to catch up. This is especially necessary in
// the OnClipboardChange label, where sometimes a clipboard-change notification comes in before the owning
// app has finished preparing its data for subsequent readers of the clipboard.
{
/*
#ifdef DEBUG_BY_LOGGING_CLIPBOARD_FORMATS // Provides a convenient log of clipboard formats for analysis.
static FILE *fp = fopen("c:\\debug_clipboard_formats.txt", "w");
#endif
char format_name[MAX_PATH + 1]; // MSDN's RegisterClipboardFormat() doesn't document any max length, but the ones we're interested in certainly don't exceed MAX_PATH.
if (uFormat < 0xC000 || uFormat > 0xFFFF) // It's a registered format (you're supposted to verify in-range before calling GetClipboardFormatName()). Also helps performance.
*format_name = '\0'; // Don't need the name if it's a standard/CF_* format.
else
{
// v1.0.42.04:
// Probably need to call GetClipboardFormatName() rather than comparing directly to uFormat because
// MSDN implies that OwnerLink and other registered formats might not always have the same ID under
// all OSes (past and future).
GetClipboardFormatName(uFormat, format_name, MAX_PATH);
// Since RegisterClipboardFormat() is case insensitive, the case might vary. So use stricmp() when
// comparing format_name to anything.
// "Link Source", "Link Source Descriptor" , and anything else starting with "Link Source" is likely
// to be data that should not be attempted to be retrieved because:
// 1) It causes unwanted bookmark effects in various versions of MS Word.
// 2) Tests show that these formats are on the clipboard only if MS Word is open at the time
// ClipboardAll is accessed. That implies they're transitory formats that aren't as essential
// or well suited to ClipboardAll as the other formats (but if it weren't for #1 above, this
// wouldn't be enough reason to omit it).
// 3) Although there is hardly any documentation to be found at MSDN or elsewhere about these formats,
// it seems they're related to OLE, with further implications that the data is transitory.
// Here are the formats that Word 2002 removes from the clipboard when it the app closes:
// 0xC002 ObjectLink >>> Causes WORD bookmarking problem.
// 0xC003 OwnerLink
// 0xC00D Link Source >>> Causes WORD bookmarking problem.
// 0xC00F Link Source Descriptor >>> Doesn't directly cause bookmarking, but probably goes with above.
// 0xC0DC Hyperlink
if ( !strnicmp(format_name, "Link Source", 11) || !stricmp(format_name, "ObjectLink")
|| !stricmp(format_name, "OwnerLink")
// v1.0.44.07: The following were added to solve interference with MS Outlook's MS Word editor.
// If a hotkey like ^F1::ClipboardSave:=ClipboardAll is pressed after pressing Ctrl-C in that
// editor (perhaps only when copying HTML), two of the following error dialogs would otherwise
// be displayed (this occurs in Outlook 2002 and probably later versions):
// "An outgoing call cannot be made since the application is dispatching an input-synchronous call."
|| !stricmp(format_name, "Native") || !stricmp(format_name, "Embed Source") )
return NULL;
}
#ifdef DEBUG_BY_LOGGING_CLIPBOARD_FORMATS
fprintf(fp, "%04X\t%s\n", uFormat, format_name); // Since fclose() is never called, the program has to exit to close/release the file.
#endif
HANDLE h;
for (DWORD start_time = GetTickCount();;)
{
// Known failure conditions:
// GetClipboardData() apparently fails when the text on the clipboard is greater than a certain size
// (Even though GetLastError() reports "Operation completed successfully"). The data size at which
// this occurs is somewhere between 20 to 96 MB (perhaps depending on system's memory and CPU speed).
if (h = GetClipboardData(uFormat)) // Assign
return h;
// It failed, so act according to the type of format and the timeout that's in effect.
// Certain standard (numerically constant) clipboard formats are known to validly yield NULL from a
// call to GetClipboardData(). Never retry these because it would only cause unnecessary delays
// (i.e. a failure until timeout).
// v1.0.42.04: More importantly, retrying them appears to cause problems with saving a Word/Excel
// clipboard via ClipboardAll.
if (uFormat == CF_HDROP // This format can fail "normally" for the reasons described at "clipboard_contains_files".
|| !stricmp(format_name, "OwnerLink")) // Known to validly yield NULL from a call to GetClipboardData(), so don't retry it to avoid having to wait the full timeout period.
return NULL;
if (g_ClipboardTimeout != -1) // We were not told to wait indefinitely and...
if (!g_ClipboardTimeout // ...we were told to make only one attempt, or ...
|| (int)(g_ClipboardTimeout - (GetTickCount() - start_time)) <= SLEEP_INTERVAL_HALF) //...it timed out.
// Above must cast to int or any negative result will be lost due to DWORD type.
return NULL;
// Use SLEEP_WITHOUT_INTERRUPTION to prevent MainWindowProc() from accepting new hotkeys
// during our operation, since a new hotkey subroutine might interfere with
// what we're doing here (e.g. if it tries to use the clipboard, or perhaps overwrites
// the deref buffer if this object's caller gave it any pointers into that memory area):
SLEEP_WITHOUT_INTERRUPTION(INTERVAL_UNSPECIFIED)
}
*/
}
ResultType Clipboard::Open()
{
/*
if (mIsOpen)
return OK;
for (DWORD start_time = GetTickCount();;)
{
if (OpenClipboard(g_hWnd))
{
mIsOpen = true;
return OK;
}
if (g_ClipboardTimeout != -1) // We were not told to wait indefinitely...
if (!g_ClipboardTimeout // ...and we were told to make only one attempt, or ...
|| (int)(g_ClipboardTimeout - (GetTickCount() - start_time)) <= SLEEP_INTERVAL_HALF) //...it timed out.
// Above must cast to int or any negative result will be lost due to DWORD type.
return FAIL;
// Use SLEEP_WITHOUT_INTERRUPTION to prevent MainWindowProc() from accepting new hotkeys
// during our operation, since a new hotkey subroutine might interfere with
// what we're doing here (e.g. if it tries to use the clipboard, or perhaps overwrites
// the deref buffer if this object's caller gave it any pointers into that memory area):
SLEEP_WITHOUT_INTERRUPTION(INTERVAL_UNSPECIFIED)
}
*/
}