Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow processing clipboard containing "secrets" #2794

Merged
merged 1 commit into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading