Skip to content

Commit

Permalink
Allow processing clipboard containing "secrets"
Browse files Browse the repository at this point in the history
This allows processing the "secret" clipboard data by overriding new
`onSecretClipboardChanged()` script function.

This also ensures that callbacks are called consistently with properly
set formats `mimeClipboardMode`, `mimeOutputTab` and `mimeCurrentTab`.

Fixes #2787
  • Loading branch information
hluk committed Jul 31, 2024
1 parent 5751cdf commit edfc9cf
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 99 deletions.
26 changes: 21 additions & 5 deletions docs/scripting-api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -1482,20 +1483,37 @@ 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.

Default implementation calls :js:func:`updateClipboardData`.

.. 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.
Expand Down Expand Up @@ -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'.
Expand Down
82 changes: 41 additions & 41 deletions src/app/clipboardmonitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -79,7 +84,8 @@ ClipboardMonitor::ClipboardMonitor(const QStringList &formats)
m_storeSelection = config.option<Config::check_selection>();
m_runSelection = config.option<Config::run_selection>();

m_clipboardToSelection = config.option<Config::copy_clipboard>();
m_clipboardToSelection = config.option<Config::copy_clipboard>()
&& m_clipboard->isSelectionSupported();
m_selectionToClipboard = config.option<Config::copy_selection>();

if (!m_storeSelection && !m_runSelection && !m_selectionToClipboard) {
Expand Down Expand Up @@ -122,33 +128,47 @@ 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() )
{
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);
Expand All @@ -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);
}
}
13 changes: 5 additions & 8 deletions src/app/clipboardmonitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <QVariantMap>

enum class ClipboardOwnership {
Foreign,
Own,
Hidden,
};

class ClipboardMonitor final : public QObject
{
Q_OBJECT
Expand All @@ -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);
Expand Down
7 changes: 4 additions & 3 deletions src/gui/commandcompleterdocumentation.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
Expand Down
27 changes: 9 additions & 18 deletions src/platform/dummy/dummyclipboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,6 @@
#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 All @@ -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)
Expand All @@ -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;
}
Expand Down
8 changes: 6 additions & 2 deletions src/platform/x11/x11platformclipboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

#include <QClipboard>
#include <QMimeData>
#include <QPointer>

namespace {

Expand Down Expand Up @@ -245,7 +246,7 @@ void X11PlatformClipboard::updateClipboardData(X11PlatformClipboard::ClipboardDa
return;
}

const auto data = mimeData(clipboardData->mode);
const QPointer<const QMimeData> data( mimeData(clipboardData->mode) );

// Retry to retrieve clipboard data few times.
if (!data) {
Expand Down Expand Up @@ -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);
Expand Down
48 changes: 33 additions & 15 deletions src/scriptable/scriptable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) ) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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");
}

Expand Down
Loading

0 comments on commit edfc9cf

Please sign in to comment.