diff --git a/docs/scripting-api.rst b/docs/scripting-api.rst index 5140e47e52..0f32b5b8f2 100644 --- a/docs/scripting-api.rst +++ b/docs/scripting-api.rst @@ -1465,7 +1465,8 @@ unlike in GUI, where row numbers start from 1 by default. .. js:function:: onClipboardChanged() - Called when clipboard or `Linux mouse selection`_ changes. + Called when clipboard or `Linux mouse selection`_ changes and is not set by + CopyQ, is not marked as hidden nor secret (see the other callbacks). Default implementation is: @@ -1482,7 +1483,8 @@ unlike in GUI, where row numbers start from 1 by default. .. js:function:: onOwnClipboardChanged() - Called when clipboard or `Linux mouse selection`_ changes by a CopyQ instance. + Called when clipboard or `Linux mouse selection`_ is set by CopyQ and is not + marked as hidden nor secret (see the other callbacks). Owned clipboard data contains :js:data:`mimeOwner` format. @@ -1490,12 +1492,28 @@ unlike in GUI, where row numbers start from 1 by default. .. js:function:: onHiddenClipboardChanged() - Called when hidden clipboard or `Linux mouse selection`_ changes. + Called when clipboard or `Linux mouse selection`_ changes and is marked as + hidden but not secret (see the other callbacks). Hidden clipboard data contains :js:data:`mimeHidden` format set to ``1``. Default implementation calls :js:func:`updateClipboardData`. +.. js:function:: onSecretClipboardChanged() + + Called if the clipboard or `Linux mouse selection`_ changes and contains a + password or other secret (for example, copied from clipboard manager). + + The default implementation clears all data, so they are not accessible using + :js:func:`data` and :js:func:`dataFormats`, except :js:data:`mimeSecret`, + and calls :js:func:`updateClipboardData`. + + **Be careful overriding** this function (via a Script command). Calling + `onClipboardChanged()` without clearing the data and without any further + checks can cause storing and processing secrets from password managers. On + the other hand, it can help to get access to the data copied, for example + from a web browser in private mode. + .. js:function:: onClipboardUnchanged() Called when clipboard or `Linux mouse selection`_ changes but data remained the same. @@ -2212,8 +2230,6 @@ These MIME types values are assigned to global variables prefixed with 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'. diff --git a/src/app/clipboardmonitor.cpp b/src/app/clipboardmonitor.cpp index 179ad90f6a..d3d8d0b3ea 100644 --- a/src/app/clipboardmonitor.cpp +++ b/src/app/clipboardmonitor.cpp @@ -45,6 +45,11 @@ bool isClipboardDataHidden(const QVariantMap &data) return data.value(mimeHidden).toByteArray() == "1"; } +bool isClipboardDataSecret(const QVariantMap &data) +{ + return data.value(mimeSecret).toByteArray() == "1"; +} + int defaultOwnerUpdateInterval() { #ifdef COPYQ_WS_X11 @@ -79,7 +84,8 @@ ClipboardMonitor::ClipboardMonitor(const QStringList &formats) m_storeSelection = config.option(); m_runSelection = config.option(); - m_clipboardToSelection = config.option(); + m_clipboardToSelection = config.option() + && m_clipboard->isSelectionSupported(); m_selectionToClipboard = config.option(); if (!m_storeSelection && !m_runSelection && !m_selectionToClipboard) { @@ -122,19 +128,15 @@ void ClipboardMonitor::onClipboardChanged(ClipboardMode mode) auto clipboardData = mode == ClipboardMode::Clipboard ? &m_clipboardData : &m_selectionData; - if ( hasSameData(data, *clipboardData) ) { + const bool isDataUnchanged = hasSameData(data, *clipboardData); + if (!isDataUnchanged) { + *clipboardData = data; #ifdef HAS_MOUSE_SELECTIONS - if ( !m_runSelection && mode == ClipboardMode::Selection ) - return; -#endif - COPYQ_LOG( QString("Ignoring unchanged %1") - .arg(mode == ClipboardMode::Clipboard ? "clipboard" : "selection") ); - emit clipboardUnchanged(data); + } else if (isDataUnchanged && !m_runSelection && mode == ClipboardMode::Selection) { return; +#endif } - *clipboardData = data; - if ( !data.contains(mimeOwner) && !data.contains(mimeWindowTitle) && !m_clipboardOwner.isEmpty() ) @@ -142,13 +144,31 @@ void ClipboardMonitor::onClipboardChanged(ClipboardMode mode) data.insert(mimeWindowTitle, m_clipboardOwner.toUtf8()); } - COPYQ_LOG( QString("%1 changed, owner is \"%2\"") - .arg(mode == ClipboardMode::Clipboard ? "Clipboard" : "Selection", + COPYQ_LOG( QStringLiteral("%1 changed, owner is: %2") + .arg(QLatin1String(mode == ClipboardMode::Clipboard ? "Clipboard" : "Selection"), getTextData(data, mimeOwner)) ); + const auto defaultTab = m_clipboardTab.isEmpty() ? defaultClipboardTabName() : m_clipboardTab; + setTextData(&data, defaultTab, mimeCurrentTab); + +#ifdef HAS_MOUSE_SELECTIONS + if (mode == ClipboardMode::Selection) + data.insert(mimeClipboardMode, QByteArrayLiteral("selection")); + + if (mode == ClipboardMode::Clipboard ? m_storeClipboard : m_storeSelection) { +#else + if (m_storeClipboard) { +#endif + setTextData(&data, m_clipboardTab, mimeOutputTab); + } + + if (isDataUnchanged) { + emit clipboardUnchanged(data); + return; + } + #ifdef HAS_MOUSE_SELECTIONS if ( (mode == ClipboardMode::Clipboard ? m_clipboardToSelection : m_selectionToClipboard) - && m_clipboard->isSelectionSupported() && !data.contains(mimeOwner) ) { const auto text = getTextData(data); @@ -165,40 +185,20 @@ void ClipboardMonitor::onClipboardChanged(ClipboardMode mode) // omit running run automatic commands when disabled if ( !m_runSelection && mode == ClipboardMode::Selection ) { - if ( m_storeSelection && !m_clipboardTab.isEmpty() ) { - data.insert(mimeClipboardMode, QByteArrayLiteral("selection")); - setTextData(&data, m_clipboardTab, mimeOutputTab); + if ( m_storeSelection && !m_clipboardTab.isEmpty() && !isClipboardDataSecret(data) ) emit saveData(data); - } return; } #endif - if (mode != ClipboardMode::Clipboard) { - const QString modeName = mode == ClipboardMode::Selection - ? QStringLiteral("selection") - : QStringLiteral("find buffer"); - data.insert(mimeClipboardMode, modeName); - } - - // run automatic commands - if ( anySessionOwnsClipboardData(data) ) { - emit clipboardChanged(data, ClipboardOwnership::Own); + // run script callbacks + if ( isClipboardDataSecret(data) ) { + emit secretClipboardChanged(data); } else if ( isClipboardDataHidden(data) ) { - emit clipboardChanged(data, ClipboardOwnership::Hidden); + emit hiddenClipboardChanged(data); + } else if ( anySessionOwnsClipboardData(data) ) { + emit ownClipboardChanged(data); } else { - const auto defaultTab = m_clipboardTab.isEmpty() ? defaultClipboardTabName() : m_clipboardTab; - setTextData(&data, defaultTab, mimeCurrentTab); - - -#ifdef HAS_MOUSE_SELECTIONS - if (mode == ClipboardMode::Clipboard ? m_storeClipboard : m_storeSelection) { -#else - if (m_storeClipboard) { -#endif - setTextData(&data, m_clipboardTab, mimeOutputTab); - } - - emit clipboardChanged(data, ClipboardOwnership::Foreign); + emit clipboardChanged(data); } } diff --git a/src/app/clipboardmonitor.h b/src/app/clipboardmonitor.h index 1f0d6617b1..96ad9387b3 100644 --- a/src/app/clipboardmonitor.h +++ b/src/app/clipboardmonitor.h @@ -4,18 +4,12 @@ #define CLIPBOARDMONITOR_H #include "app/clipboardownermonitor.h" +#include "common/clipboardmode.h" #include "common/common.h" #include "platform/platformnativeinterface.h" -#include "platform/platformclipboard.h" #include -enum class ClipboardOwnership { - Foreign, - Own, - Hidden, -}; - class ClipboardMonitor final : public QObject { Q_OBJECT @@ -27,7 +21,10 @@ class ClipboardMonitor final : public QObject void setClipboardOwner(const QString &owner); signals: - void clipboardChanged(const QVariantMap &data, ClipboardOwnership ownership); + void clipboardChanged(const QVariantMap &data); + void secretClipboardChanged(const QVariantMap &data); + void hiddenClipboardChanged(const QVariantMap &data); + void ownClipboardChanged(const QVariantMap &data); void clipboardUnchanged(const QVariantMap &data); void saveData(const QVariantMap &data); void synchronizeSelection(ClipboardMode sourceMode, uint sourceTextHash, uint targetTextHash); diff --git a/src/gui/commandcompleterdocumentation.h b/src/gui/commandcompleterdocumentation.h index 578f42e0df..13960d8a81 100644 --- a/src/gui/commandcompleterdocumentation.h +++ b/src/gui/commandcompleterdocumentation.h @@ -153,9 +153,10 @@ void addDocumentation(AddDocumentationCallback addDocumentation) addDocumentation("iconTagColor", "iconTagColor() -> string", "Get current tray and window tag color name."); addDocumentation("iconTagColor", "iconTagColor(colorName)", "Set current tray and window tag color name."); addDocumentation("loadTheme", "loadTheme(path)", "Loads theme from an INI file."); - addDocumentation("onClipboardChanged", "onClipboardChanged()", "Called when clipboard or `Linux mouse selection`_ changes."); - addDocumentation("onOwnClipboardChanged", "onOwnClipboardChanged()", "Called when clipboard or `Linux mouse selection`_ changes by a CopyQ instance."); - addDocumentation("onHiddenClipboardChanged", "onHiddenClipboardChanged()", "Called when hidden clipboard or `Linux mouse selection`_ changes."); + addDocumentation("onClipboardChanged", "onClipboardChanged()", "Called when clipboard or `Linux mouse selection`_ changes and is not set by CopyQ, is not marked as hidden nor secret (see the other callbacks)."); + addDocumentation("onOwnClipboardChanged", "onOwnClipboardChanged()", "Called when clipboard or `Linux mouse selection`_ is set by CopyQ and is not marked as hidden nor secret (see the other callbacks)."); + addDocumentation("onHiddenClipboardChanged", "onHiddenClipboardChanged()", "Called when clipboard or `Linux mouse selection`_ changes and is marked as hidden but not secret (see the other callbacks)."); + addDocumentation("onSecretClipboardChanged", "onSecretClipboardChanged()", "Called if the clipboard or `Linux mouse selection`_ changes and contains a password or other secret (for example, copied from clipboard manager)."); addDocumentation("onClipboardUnchanged", "onClipboardUnchanged()", "Called when clipboard or `Linux mouse selection`_ changes but data remained the same."); addDocumentation("onStart", "onStart()", "Called when application starts."); addDocumentation("onExit", "onExit()", "Called just before application exists."); diff --git a/src/platform/dummy/dummyclipboard.cpp b/src/platform/dummy/dummyclipboard.cpp index 867c6bd468..18819ef1e1 100644 --- a/src/platform/dummy/dummyclipboard.cpp +++ b/src/platform/dummy/dummyclipboard.cpp @@ -10,17 +10,6 @@ #include #include -namespace { - -const QMimeData *createSecretData() -{ - auto data = new QMimeData(); - data->setData(mimeSecret, QByteArrayLiteral("1")); - return data; -} - -} // namespace - QClipboard::Mode modeToQClipboardMode(ClipboardMode mode) { switch (mode) { @@ -43,7 +32,15 @@ void DummyClipboard::startMonitoring(const QStringList &) QVariantMap DummyClipboard::data(ClipboardMode mode, const QStringList &formats) const { const QMimeData *data = mimeData(mode); - return data ? cloneData(*data, formats) : QVariantMap(); + if (data == nullptr) + return {}; + + const bool isDataSecret = isHidden(*data); + QVariantMap dataMap = cloneData(*data, formats); + if (isDataSecret) + dataMap[mimeSecret] = QByteArrayLiteral("1"); + + return dataMap; } void DummyClipboard::setData(ClipboardMode mode, const QVariantMap &dataMap) @@ -68,12 +65,6 @@ const QMimeData *DummyClipboard::mimeData(ClipboardMode mode) const return nullptr; } - if (isHidden(*data)) { - log( QStringLiteral("Hiding secret %1 data").arg(modeText) ); - static const QMimeData *secretData = createSecretData(); - return secretData; - } - COPYQ_LOG_VERBOSE( QStringLiteral("Got %1 data").arg(modeText) ); return data; } diff --git a/src/platform/x11/x11platformclipboard.cpp b/src/platform/x11/x11platformclipboard.cpp index 13d69478a8..ecae271c37 100644 --- a/src/platform/x11/x11platformclipboard.cpp +++ b/src/platform/x11/x11platformclipboard.cpp @@ -22,6 +22,7 @@ #include #include +#include namespace { @@ -245,7 +246,7 @@ void X11PlatformClipboard::updateClipboardData(X11PlatformClipboard::ClipboardDa return; } - const auto data = mimeData(clipboardData->mode); + const QPointer data( mimeData(clipboardData->mode) ); // Retry to retrieve clipboard data few times. if (!data) { @@ -283,14 +284,17 @@ void X11PlatformClipboard::updateClipboardData(X11PlatformClipboard::ClipboardDa // text did not change. if ( newDataTimestamp != 0 && clipboardData->newDataTimestamp == newDataTimestamp ) { const QVariantMap newData = cloneData(*data, {mimeText}); - if (newData.value(mimeText) == clipboardData->newData.value(mimeText)) + if (!data || newData.value(mimeText) == clipboardData->newData.value(mimeText)) return; } clipboardData->timerEmitChange.stop(); clipboardData->abortCloning = false; clipboardData->cloningData = true; + const bool isDataSecret = isHidden(*data); clipboardData->newData = cloneData(*data, clipboardData->formats, &clipboardData->abortCloning); + if (isDataSecret) + clipboardData->newData[mimeSecret] = QByteArrayLiteral("1"); clipboardData->cloningData = false; if (clipboardData->abortCloning) { m_timerCheckAgain.setInterval(0); diff --git a/src/scriptable/scriptable.cpp b/src/scriptable/scriptable.cpp index 3226e3454d..9e57c5b61d 100644 --- a/src/scriptable/scriptable.cpp +++ b/src/scriptable/scriptable.cpp @@ -2382,6 +2382,13 @@ void Scriptable::onClipboardUnchanged() { } +void Scriptable::onSecretClipboardChanged() +{ + // Drop secret data by default + m_data = {{mimeSecret, m_data.value(mimeSecret)}}; + eval("updateClipboardData()"); +} + void Scriptable::synchronizeToSelection() { if ( canSynchronizeSelection(ClipboardMode::Selection) ) { @@ -2605,6 +2612,12 @@ void Scriptable::monitorClipboard() connect(this, &Scriptable::finished, &loop, &QEventLoop::quit); connect( &monitor, &ClipboardMonitor::clipboardChanged, this, &Scriptable::onMonitorClipboardChanged ); + connect( &monitor, &ClipboardMonitor::hiddenClipboardChanged, + this, &Scriptable::onMonitorHiddenClipboardChanged ); + connect( &monitor, &ClipboardMonitor::ownClipboardChanged, + this, &Scriptable::onMonitorOwnClipboardChanged ); + connect( &monitor, &ClipboardMonitor::secretClipboardChanged, + this, &Scriptable::onMonitorSecretClipboardChanged ); connect( &monitor, &ClipboardMonitor::clipboardUnchanged, this, &Scriptable::onMonitorClipboardUnchanged ); connect( &monitor, &ClipboardMonitor::synchronizeSelection, @@ -2684,28 +2697,33 @@ void Scriptable::collectScriptOverrides() m_proxy->setScriptOverrides(overrides); } -void Scriptable::onMonitorClipboardChanged(const QVariantMap &data, ClipboardOwnership ownership) +void Scriptable::onMonitorClipboardChanged(const QVariantMap &data) +{ + COPYQ_LOG("onClipboardChanged"); + m_proxy->runInternalAction(data, QStringLiteral("copyq onClipboardChanged")); +} + +void Scriptable::onMonitorSecretClipboardChanged(const QVariantMap &data) { - COPYQ_LOG( QStringLiteral("onMonitorClipboardChanged: %1 %2, owner is \"%3\"") - .arg( - QString::fromLatin1( - ownership == ClipboardOwnership::Own ? "own" - : ownership == ClipboardOwnership::Foreign ? "foreign" - : "hidden"), - QString::fromLatin1(isClipboardData(data) ? "clipboard" : "selection"), - getTextData(data, mimeOwner) - ) ); + COPYQ_LOG("onSecretClipboardChanged"); + m_proxy->runInternalAction(data, QStringLiteral("copyq onSecretClipboardChanged")); +} - const QString command = - ownership == ClipboardOwnership::Own ? "copyq onOwnClipboardChanged" - : ownership == ClipboardOwnership::Hidden ? "copyq onHiddenClipboardChanged" - : "copyq onClipboardChanged"; +void Scriptable::onMonitorHiddenClipboardChanged(const QVariantMap &data) +{ + COPYQ_LOG("onHiddenClipboardChanged"); + m_proxy->runInternalAction(data, QStringLiteral("copyq onHiddenClipboardChanged")); +} - m_proxy->runInternalAction(data, command); +void Scriptable::onMonitorOwnClipboardChanged(const QVariantMap &data) +{ + COPYQ_LOG("onOwnClipboardChanged"); + m_proxy->runInternalAction(data, QStringLiteral("copyq onOwnClipboardChanged")); } void Scriptable::onMonitorClipboardUnchanged(const QVariantMap &data) { + COPYQ_LOG("onOwnClipboardUnchanged"); m_proxy->runInternalAction(data, "copyq onClipboardUnchanged"); } diff --git a/src/scriptable/scriptable.h b/src/scriptable/scriptable.h index 23b0206410..44f26e7be2 100644 --- a/src/scriptable/scriptable.h +++ b/src/scriptable/scriptable.h @@ -361,6 +361,7 @@ public slots: void onOwnClipboardChanged(); void onHiddenClipboardChanged(); void onClipboardUnchanged(); + void onSecretClipboardChanged(); void onStart() {} void onExit() {} @@ -405,7 +406,10 @@ public slots: void receiveData(); private: - void onMonitorClipboardChanged(const QVariantMap &data, ClipboardOwnership ownership); + void onMonitorClipboardChanged(const QVariantMap &data); + void onMonitorSecretClipboardChanged(const QVariantMap &data); + void onMonitorHiddenClipboardChanged(const QVariantMap &data); + void onMonitorOwnClipboardChanged(const QVariantMap &data); void onMonitorClipboardUnchanged(const QVariantMap &data); void onSynchronizeSelection(ClipboardMode sourceMode, uint sourceTextHash, uint targetTextHash); void onFetchCurrentClipboardOwner(QString *title); diff --git a/src/tests/tests.cpp b/src/tests/tests.cpp index a3dc6f923e..a6f59fc91b 100644 --- a/src/tests/tests.cpp +++ b/src/tests/tests.cpp @@ -4854,14 +4854,13 @@ void Tests::startServerAndRunCommand() void Tests::avoidStoringPasswords() { TEST( m_test->setClipboard(secretData("secret")) ); - waitFor(2 * waitMsPasteClipboard); - RUN("clipboard" << "?", mimeSecret + "\n"); + WAIT_ON_OUTPUT("clipboard", "secret"); 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("read" << "0" << "1" << "2", "secret\n\n"); RUN("count", "1\n"); } @@ -4870,15 +4869,14 @@ void Tests::scriptsForPasswords() const auto script = R"( setCommands([{ isScript: true, - cmd: `global.updateClipboardData = function() { - if (data(mimeSecret) == "1") add("SECRET"); + cmd: `global.onSecretClipboardChanged = function() { + 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"); }