Skip to content

Commit

Permalink
Allow handling secret clipboard and other changes
Browse files Browse the repository at this point in the history
Allows running scripts even if clipboard data was copied from a
clipboard manager or an internal data format (`application/x-copyq-*`)
changed.

Fixes #2746
  • Loading branch information
hluk committed Jul 11, 2024
1 parent 97861f0 commit 64d9802
Show file tree
Hide file tree
Showing 12 changed files with 72 additions and 45 deletions.
6 changes: 6 additions & 0 deletions docs/scripting-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2208,6 +2208,12 @@ These MIME types values are assigned to global variables prefixed with

copyq copy application/x-copyq-hidden 1 plain/text "This is secret"

.. js:data:: mimeSecret

If set to ``1``, the clipboard contains a password or other secret (for example, copied from clipboard manager).

In such case, the data won't be available in the app, not even via calling ``data()`` script function.

.. js:data:: mimeShortcut

Application or global shortcut which activated the command. Value: 'application/x-copyq-shortcut'.
Expand Down
21 changes: 3 additions & 18 deletions src/app/clipboardmonitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,15 @@ namespace {

bool hasSameData(const QVariantMap &data, const QVariantMap &lastData)
{
// Detect change also in case the data is unchanged but previously copied
// by CopyQ and now externally. This solves storing a copied text which was
// previously synchronized from selection to clipboard via CopyQ.
if (
!lastData.value(mimeOwner).toByteArray().isEmpty()
&& data.value(mimeOwner).toByteArray().isEmpty()
)
{
return false;
}

for (auto it = lastData.constBegin(); it != lastData.constEnd(); ++it) {
const auto &format = it.key();
if ( !format.startsWith(COPYQ_MIME_PREFIX)
&& !data.contains(format) )
{
if ( !data.contains(format) )
return false;
}
}

for (auto it = data.constBegin(); it != data.constEnd(); ++it) {
const auto &format = it.key();
if ( !format.startsWith(COPYQ_MIME_PREFIX)
&& !data[format].toByteArray().isEmpty()
if ( !data[format].toByteArray().isEmpty()
&& data[format] != lastData.value(format) )
{
return false;
Expand Down Expand Up @@ -84,7 +69,7 @@ ClipboardMonitor::ClipboardMonitor(const QStringList &formats)
m_ownerMonitor.setUpdateInterval(
ownerUpdateInterval < 0 ? defaultOwnerUpdateInterval() : ownerUpdateInterval);

m_formats.append({mimeOwner, mimeWindowTitle, mimeItemNotes, mimeHidden});
m_formats.append({mimeOwner, mimeWindowTitle, mimeItemNotes, mimeHidden, mimeSecret});
m_formats.removeDuplicates();

connect( m_clipboard.get(), &PlatformClipboard::changed,
Expand Down
2 changes: 1 addition & 1 deletion src/common/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ QString textLabelForData(const QVariantMap &data, const QFont &font, const QStri

const QString notes = data.value(mimeItemNotes).toString();

if ( data.contains(mimeHidden) ) {
if ( data.contains(mimeHidden) || data.contains(mimeSecret) ) {
label = QObject::tr("<HIDDEN>", "Label for hidden/secret clipboard content");
} else if ( data.contains(mimeText) || data.contains(mimeUriList) ) {
const QString text = getTextData(data);
Expand Down
1 change: 1 addition & 0 deletions src/common/mimetypes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ const QLatin1String mimeShortcut(COPYQ_MIME_PREFIX "shortcut");
const QLatin1String mimeColor(COPYQ_MIME_PREFIX "color");
const QLatin1String mimeOutputTab(COPYQ_MIME_PREFIX "output-tab");
const QLatin1String mimeDisplayItemInMenu(COPYQ_MIME_PREFIX "display-item-in-menu");
const QLatin1String mimeSecret(COPYQ_MIME_PREFIX "secret");
1 change: 1 addition & 0 deletions src/common/mimetypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ extern const QLatin1String mimeShortcut;
extern const QLatin1String mimeColor;
extern const QLatin1String mimeOutputTab;
extern const QLatin1String mimeDisplayItemInMenu;
extern const QLatin1String mimeSecret;
3 changes: 2 additions & 1 deletion src/gui/commandcompleterdocumentation.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ void addDocumentation(AddDocumentationCallback addDocumentation)
addDocumentation("sha256sum", "sha256sum(data) -> `ByteArray`", "Returns SHA256 checksum of data.");
addDocumentation("sha512sum", "sha512sum(data) -> `ByteArray`", "Returns SHA512 checksum of data.");
addDocumentation("open", "open(url, ...) -> bool", "Tries to open URLs in appropriate applications.");
addDocumentation("execute", "execute(argument, ..., null, stdinData, ...) -> `FinishedCommand` or `undefined`", "Executes a command.");
addDocumentation("execute", "execute(argument, ..., null, stdinData, ...) -> `FinishedCommand`", "Executes a command.");
addDocumentation("currentWindowTitle", "String currentWindowTitle() -> string", "Returns window title of currently focused window.");
addDocumentation("currentClipboardOwner", "String currentClipboardOwner() -> string", "Returns name of the current clipboard owner.");
addDocumentation("dialog", "dialog(...)", "Shows messages or asks user for input.");
Expand Down Expand Up @@ -204,6 +204,7 @@ void addDocumentation(AddDocumentationCallback addDocumentation)
addDocumentation("mimeSelectedItems", "mimeSelectedItems", "Selected items when invoking command from main window. Value: 'application/x-copyq-selected-items'.");
addDocumentation("mimeCurrentItem", "mimeCurrentItem", "Current item when invoking command from main window. Value: 'application/x-copyq-current-item'.");
addDocumentation("mimeHidden", "mimeHidden", "If set to `1`, the clipboard or item content will be hidden in GUI. Value: 'application/x-copyq-hidden'.");
addDocumentation("mimeSecret", "mimeSecret", "If set to `1`, the clipboard contains a password or other secret (for example, copied from clipboard manager).");
addDocumentation("mimeShortcut", "mimeShortcut", "Application or global shortcut which activated the command. Value: 'application/x-copyq-shortcut'.");
addDocumentation("mimeColor", "mimeColor", "Item color (same as the one used by themes). Value: 'application/x-copyq-color'.");
addDocumentation("mimeOutputTab", "mimeOutputTab", "Name of the tab where to store new item. Value: 'application/x-copyq-output-tab'.");
Expand Down
14 changes: 13 additions & 1 deletion src/platform/dummy/dummyclipboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
#include <QMimeData>
#include <QStringList>

namespace {

const QMimeData *createSecretData()
{
auto data = new QMimeData();
data->setData(mimeSecret, QByteArrayLiteral("1"));
return data;
}

} // namespace

QClipboard::Mode modeToQClipboardMode(ClipboardMode mode)
{
switch (mode) {
Expand Down Expand Up @@ -59,7 +70,8 @@ const QMimeData *DummyClipboard::mimeData(ClipboardMode mode) const

if (isHidden(*data)) {
log( QStringLiteral("Hiding secret %1 data").arg(modeText) );
return nullptr;
static const QMimeData *secretData = createSecretData();
return secretData;
}

COPYQ_LOG_VERBOSE( QStringLiteral("Got %1 data").arg(modeText) );
Expand Down
5 changes: 0 additions & 5 deletions src/platform/x11/x11platformclipboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -269,11 +269,6 @@ void X11PlatformClipboard::updateClipboardData(X11PlatformClipboard::ClipboardDa
}
clipboardData->retry = 0;

// Ignore clipboard with secrets
const QByteArray passwordManagerHint = data->data(QStringLiteral("x-kde-passwordManagerHint"));
if ( passwordManagerHint == QByteArrayLiteral("secret") )
return;

const QByteArray newDataTimestampData = data->data(QStringLiteral("TIMESTAMP"));
quint32 newDataTimestamp = 0;
if ( !newDataTimestampData.isEmpty() ) {
Expand Down
3 changes: 2 additions & 1 deletion src/scriptable/scriptable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ bool isInternalDataFormat(const QString &format)
|| format == mimeSelectedItems
|| format == mimeCurrentItem
|| format == mimeShortcut
|| format == mimeOutputTab;
|| format == mimeOutputTab
|| format == mimeSecret;
}

QVariantMap copyWithoutInternalData(const QVariantMap &data) {
Expand Down
2 changes: 2 additions & 0 deletions src/scriptable/scriptable.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class Scriptable final : public QObject
Q_PROPERTY(QJSValue mimeColor READ getMimeColor CONSTANT)
Q_PROPERTY(QJSValue mimeOutputTab READ getMimeOutputTab CONSTANT)
Q_PROPERTY(QJSValue mimeDisplayItemInMenu READ getMimeDisplayItemInMenu CONSTANT)
Q_PROPERTY(QJSValue mimeSecret READ getMimeSecret CONSTANT)

Q_PROPERTY(QJSValue plugins READ getPlugins CONSTANT)

Expand Down Expand Up @@ -138,6 +139,7 @@ class Scriptable final : public QObject
QJSValue getMimeColor() const { return mimeColor; }
QJSValue getMimeOutputTab() const { return mimeOutputTab; }
QJSValue getMimeDisplayItemInMenu() const { return mimeDisplayItemInMenu; }
QJSValue getMimeSecret() const { return mimeSecret; }

QJSValue getPlugins();

Expand Down
58 changes: 40 additions & 18 deletions src/tests/tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,25 @@ QString appWindowTitle(const QString &text)
#endif
}

QVariantMap secretData(const QByteArray &text)
{
#ifdef Q_OS_WIN
const QString format("application/x-qt-windows-mime;value=\"Clipboard Viewer Ignore\"");
const QByteArray value("");
#elif defined(Q_OS_MACOS)
const QString format("application/x-nspasteboard-concealed-type");
const QByteArray value("secret");
#elif defined(Q_OS_UNIX)
const QString format("x-kde-passwordManagerHint");
const QByteArray value("secret");
#endif

return QVariantMap{
{format, value},
{mimeText, text},
};
}

} // namespace

Tests::Tests(const TestInterfacePtr &test, QObject *parent)
Expand Down Expand Up @@ -4799,31 +4818,34 @@ void Tests::startServerAndRunCommand()

void Tests::avoidStoringPasswords()
{
#ifdef Q_OS_WIN
const QString format("application/x-qt-windows-mime;value=\"Clipboard Viewer Ignore\"");
const QByteArray value("");
#elif defined(Q_OS_MACOS)
const QString format("application/x-nspasteboard-concealed-type");
const QByteArray value("secret");
#elif defined(Q_OS_UNIX)
const QString format("x-kde-passwordManagerHint");
const QByteArray value("secret");
#endif

const QVariantMap data{
{format, value},
{mimeText, QByteArrayLiteral("secret")},
};
TEST( m_test->setClipboard(data) );
TEST( m_test->setClipboard(secretData("secret")) );
waitFor(2 * waitMsPasteClipboard);
RUN("clipboard" << "?", "");
RUN("clipboard" << "?", mimeSecret + "\n");
RUN("read" << "0" << "1" << "2", "\n\n");
RUN("count", "0\n");

RUN("keys" << clipboardBrowserId << keyNameFor(QKeySequence::Paste), "");
waitFor(waitMsPasteClipboard);
RUN("read" << "0" << "1" << "2", "\n\n");
RUN("count", "0\n");
RUN("count", "1\n");
}

void Tests::scriptsForPasswords()
{
const auto script = R"(
setCommands([{
isScript: true,
cmd: `global.updateClipboardData = function() {
if (data(mimeSecret) == "1") add("SECRET");
}`
}])
)";
RUN(script, "");
WAIT_ON_OUTPUT("commands().length", "1\n");
TEST( m_test->setClipboard(secretData("secret")) );
waitFor(2 * waitMsPasteClipboard);
WAIT_ON_OUTPUT("read" << "0" << "1" << "2", "SECRET\n\n");
RUN("count", "1\n");
}

void Tests::currentClipboardOwner()
Expand Down
1 change: 1 addition & 0 deletions src/tests/tests.h
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ private slots:
void startServerAndRunCommand();

void avoidStoringPasswords();
void scriptsForPasswords();

void currentClipboardOwner();

Expand Down

0 comments on commit 64d9802

Please sign in to comment.