diff --git a/src/common/clipboarddataguard.cpp b/src/common/clipboarddataguard.cpp new file mode 100644 index 0000000000..ccb2030f6e --- /dev/null +++ b/src/common/clipboarddataguard.cpp @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "common/clipboarddataguard.h" +#include "common/common.h" +#include "common/log.h" +#include "common/mimetypes.h" +#include "common/textdata.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +int clipboardCopyTimeoutMs() +{ + static bool ok = false; + static int ms = qgetenv("COPYQ_CLIPBOARD_COPY_TIMEOUT_MS").toInt(&ok); + return ok ? ms : 5000; +} + +const QMimeData *dummyMimeData() +{ + static QMimeData mimeData; + return &mimeData; +} + +class ElapsedGuard { +public: + explicit ElapsedGuard(const QString &type, const QString &format) + : m_type(type) + , m_format(format) + { + COPYQ_LOG_VERBOSE( QStringLiteral("Accessing [%1:%2]").arg(type, format) ); + m_elapsed.start(); + } + + ~ElapsedGuard() + { + const auto t = m_elapsed.elapsed(); + if (t > 500) + log( QStringLiteral("ELAPSED %1 ms accessing [%2:%3]").arg(t).arg(m_type, m_format), LogWarning ); + } +private: + QString m_type; + QString m_format; + QElapsedTimer m_elapsed; +}; + +} //namespace + +ClipboardDataGuard::ClipboardDataGuard(const QMimeData *data, const long int *clipboardSequenceNumber) + : m_data(data) + , m_clipboardSequenceNumber(clipboardSequenceNumber) + , m_clipboardSequenceNumberOriginal(clipboardSequenceNumber ? *clipboardSequenceNumber : 0) +{ + // This uses simple connection to ensure pointer is not destroyed + // instead of QPointer to work around a possible Qt bug: + // - https://bugzilla.redhat.com/show_bug.cgi?id=2320093 + // - https://bugzilla.redhat.com/show_bug.cgi?id=2326881 + m_connection = QObject::connect(m_data, &QObject::destroyed, [this](){ + m_data = nullptr; + log( QByteArrayLiteral("Aborting clipboard cloning: Data deleted"), LogWarning ); + }); + m_timerExpire.start(); +} + +ClipboardDataGuard::~ClipboardDataGuard() +{ + QObject::disconnect(m_connection); +} + +QStringList ClipboardDataGuard::formats() +{ + ElapsedGuard _(QStringLiteral(), QStringLiteral("formats")); + return mimeData()->formats(); +} + +bool ClipboardDataGuard::hasFormat(const QString &mime) +{ + ElapsedGuard _(QStringLiteral("hasFormat"), mime); + return mimeData()->hasFormat(mime); +} + +QByteArray ClipboardDataGuard::data(const QString &mime) +{ + ElapsedGuard _(QStringLiteral("data"), mime); + return mimeData()->data(mime); +} + +QList ClipboardDataGuard::urls() +{ + ElapsedGuard _(QStringLiteral(), QStringLiteral("urls")); + return mimeData()->urls(); +} + +QString ClipboardDataGuard::text() +{ + ElapsedGuard _(QStringLiteral(), QStringLiteral("text")); + return mimeData()->text(); +} + +bool ClipboardDataGuard::hasText() +{ + ElapsedGuard _(QStringLiteral(), QStringLiteral("hasText")); + return mimeData()->hasText(); +} + +QImage ClipboardDataGuard::getImageData() +{ + ElapsedGuard _(QStringLiteral(), QStringLiteral("imageData")); + + // NOTE: Application hangs if using multiple sessions and + // calling QMimeData::hasImage() on X11 clipboard. + QImage image = mimeData()->imageData().value(); + if ( image.isNull() ) { + image.loadFromData( data(QStringLiteral("image/png")), "png" ); + if ( image.isNull() ) { + image.loadFromData( data(QStringLiteral("image/bmp")), "bmp" ); + } + } + COPYQ_LOG_VERBOSE( + QStringLiteral("Image is %1") + .arg(image.isNull() ? QStringLiteral("invalid") : QStringLiteral("valid")) ); + return image; +} + +QByteArray ClipboardDataGuard::getUtf8Data(const QString &format) +{ + ElapsedGuard _(QStringLiteral("UTF8"), format); + + if (format == mimeUriList) { + QByteArray bytes; + for ( const auto &url : urls() ) { + if ( !bytes.isEmpty() ) + bytes += '\n'; + bytes += url.toString().toUtf8(); + } + return bytes; + } + + if ( format == mimeText && !hasFormat(mimeText) ) + return text().toUtf8(); + + if ( format.startsWith(QLatin1String("text/")) ) + return dataToText( data(format), format ).toUtf8(); + + return data(format); +} + +bool ClipboardDataGuard::isExpired() { + if (!m_data) + return true; + + if (m_clipboardSequenceNumber && *m_clipboardSequenceNumber != m_clipboardSequenceNumberOriginal) { + m_data = nullptr; + log( QByteArrayLiteral("Aborting clipboard cloning: Clipboard changed again"), LogWarning ); + return true; + } + + if (m_timerExpire.elapsed() > clipboardCopyTimeoutMs()) { + m_data = nullptr; + log( QByteArrayLiteral("Aborting clipboard cloning: Data access took too long"), LogWarning ); + return true; + } + + return false; +} + +const QMimeData *ClipboardDataGuard::mimeData() +{ + if (isExpired()) + return dummyMimeData(); + + if (m_timerExpire.elapsed() > 100) { + QCoreApplication::processEvents(); + if (isExpired()) + return dummyMimeData(); + } + + return m_data; +} diff --git a/src/common/clipboarddataguard.h b/src/common/clipboarddataguard.h new file mode 100644 index 0000000000..41ec72300b --- /dev/null +++ b/src/common/clipboarddataguard.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include +#include + +class QMimeData; +class QImage; +class QUrl; + +// Avoids accessing old clipboard/drag'n'drop data. +class ClipboardDataGuard final { +public: + explicit ClipboardDataGuard( + const QMimeData *data, + const long int *clipboardSequenceNumber = nullptr); + + ~ClipboardDataGuard(); + + QStringList formats(); + bool hasFormat(const QString &mime); + + QByteArray data(const QString &mime); + + QList urls(); + QString text(); + bool hasText(); + QImage getImageData(); + QByteArray getUtf8Data(const QString &format); + bool isExpired(); + const QMimeData *mimeData(); + +private: + const QMimeData *m_data; + QElapsedTimer m_timerExpire; + const long int *m_clipboardSequenceNumber; + long int m_clipboardSequenceNumberOriginal; + QMetaObject::Connection m_connection; +}; diff --git a/src/common/common.cpp b/src/common/common.cpp index af4219a956..0d718d86ac 100644 --- a/src/common/common.cpp +++ b/src/common/common.cpp @@ -2,6 +2,7 @@ #include "common/common.h" +#include "common/clipboarddataguard.h" #include "common/display.h" #include "common/log.h" #include "common/mimetypes.h" @@ -39,19 +40,6 @@ namespace { const int maxElidedTextLineLength = 512; -int clipboardCopyTimeoutMs() -{ - static bool ok = false; - static int ms = qgetenv("COPYQ_CLIPBOARD_COPY_TIMEOUT_MS").toInt(&ok); - return ok ? ms : 5000; -} - -const QMimeData *dummyMimeData() -{ - static QMimeData mimeData; - return &mimeData; -} - class MimeData final : public QMimeData { protected: #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) @@ -65,170 +53,6 @@ class MimeData final : public QMimeData { } }; -// Avoids accessing old clipboard/drag'n'drop data. -class ClipboardDataGuard final { -public: - class ElapsedGuard { - public: - explicit ElapsedGuard(const QString &type, const QString &format) - : m_type(type) - , m_format(format) - { - COPYQ_LOG_VERBOSE( QStringLiteral("Accessing [%1:%2]").arg(type, format) ); - m_elapsed.start(); - } - - ~ElapsedGuard() - { - const auto t = m_elapsed.elapsed(); - if (t > 500) - log( QStringLiteral("ELAPSED %1 ms accessing [%2:%3]").arg(t).arg(m_type, m_format), LogWarning ); - } - private: - QString m_type; - QString m_format; - QElapsedTimer m_elapsed; - }; - - explicit ClipboardDataGuard(const QMimeData &data, const long int *clipboardSequenceNumber = nullptr) - : m_data(&data) - , m_clipboardSequenceNumber(clipboardSequenceNumber) - , m_clipboardSequenceNumberOriginal(clipboardSequenceNumber ? *clipboardSequenceNumber : 0) - { - // This uses simple connection to ensure pointer is not destroyed - // instead of QPointer to work around a possible Qt bug - // (https://bugzilla.redhat.com/show_bug.cgi?id=2320093). - m_connection = QObject::connect(m_data, &QObject::destroyed, [this](){ - m_data = nullptr; - log( QByteArrayLiteral("Aborting clipboard cloning: Data deleted"), LogWarning ); - }); - m_timerExpire.start(); - } - - ~ClipboardDataGuard() - { - QObject::disconnect(m_connection); - } - - QStringList formats() - { - ElapsedGuard _(QStringLiteral(), QStringLiteral("formats")); - return mimeData()->formats(); - } - - bool hasFormat(const QString &mime) - { - ElapsedGuard _(QStringLiteral("hasFormat"), mime); - return mimeData()->hasFormat(mime); - } - - QByteArray data(const QString &mime) - { - ElapsedGuard _(QStringLiteral("data"), mime); - return mimeData()->data(mime); - } - - QList urls() - { - ElapsedGuard _(QStringLiteral(), QStringLiteral("urls")); - return mimeData()->urls(); - } - - QString text() - { - ElapsedGuard _(QStringLiteral(), QStringLiteral("text")); - return mimeData()->text(); - } - - bool hasText() - { - ElapsedGuard _(QStringLiteral(), QStringLiteral("hasText")); - return mimeData()->hasText(); - } - - QImage getImageData() - { - ElapsedGuard _(QStringLiteral(), QStringLiteral("imageData")); - - // NOTE: Application hangs if using multiple sessions and - // calling QMimeData::hasImage() on X11 clipboard. - QImage image = mimeData()->imageData().value(); - if ( image.isNull() ) { - image.loadFromData( data(QStringLiteral("image/png")), "png" ); - if ( image.isNull() ) { - image.loadFromData( data(QStringLiteral("image/bmp")), "bmp" ); - } - } - COPYQ_LOG_VERBOSE( - QStringLiteral("Image is %1") - .arg(image.isNull() ? QStringLiteral("invalid") : QStringLiteral("valid")) ); - return image; - } - - QByteArray getUtf8Data(const QString &format) - { - ElapsedGuard _(QStringLiteral("UTF8"), format); - - if (format == mimeUriList) { - QByteArray bytes; - for ( const auto &url : urls() ) { - if ( !bytes.isEmpty() ) - bytes += '\n'; - bytes += url.toString().toUtf8(); - } - return bytes; - } - - if ( format == mimeText && !hasFormat(mimeText) ) - return text().toUtf8(); - - if ( format.startsWith(QLatin1String("text/")) ) - return dataToText( data(format), format ).toUtf8(); - - return data(format); - } - - bool isExpired() { - if (!m_data) - return true; - - if (m_clipboardSequenceNumber && *m_clipboardSequenceNumber != m_clipboardSequenceNumberOriginal) { - m_data = nullptr; - log( QByteArrayLiteral("Aborting clipboard cloning: Clipboard changed again"), LogWarning ); - return true; - } - - if (m_timerExpire.elapsed() > clipboardCopyTimeoutMs()) { - m_data = nullptr; - log( QByteArrayLiteral("Aborting clipboard cloning: Data access took too long"), LogWarning ); - return true; - } - - return false; - } - -private: - const QMimeData *mimeData() - { - if (isExpired()) - return dummyMimeData(); - - if (m_timerExpire.elapsed() > 100) { - QCoreApplication::processEvents(); - if (isExpired()) - return dummyMimeData(); - } - - return m_data; - } - - const QMimeData *m_data; - QElapsedTimer m_timerExpire; - const long int *m_clipboardSequenceNumber; - long int m_clipboardSequenceNumberOriginal; - QMetaObject::Connection m_connection; -}; - QString getImageFormatFromMime(const QString &mime) { static const QString imageMimePrefix = QStringLiteral("image/"); @@ -343,7 +167,9 @@ bool isBinaryImageFormat(const QString &format) && !format.contains(QStringLiteral("svg")); } -QVariantMap cloneData(ClipboardDataGuard &data, QStringList &formats) +} // namespace + +QVariantMap cloneData(ClipboardDataGuard &data, const QStringList &formats) { QVariantMap newdata; @@ -356,16 +182,13 @@ QVariantMap cloneData(ClipboardDataGuard &data, QStringList &formats) Images in SVG and other XML formats are expected to be relatively small so these doesn't have to be ignored. */ - if ( formats.contains(mimeText) && data.hasText() ) { - const auto first = std::remove_if( - std::begin(formats), std::end(formats), isBinaryImageFormat); - formats.erase(first, std::end(formats)); - } + const bool skipBinaryImageFormats = formats.contains(mimeText) && data.hasText(); QStringList imageFormats; for (const auto &mime : formats) { if (isBinaryImageFormat(mime)) { - imageFormats.append(mime); + if (!skipBinaryImageFormats) + imageFormats.append(mime); } else { const QByteArray bytes = data.getUtf8Data(mime); if ( bytes.isEmpty() ) @@ -397,15 +220,13 @@ QVariantMap cloneData(ClipboardDataGuard &data, QStringList &formats) return newdata; } -} // namespace - -QVariantMap cloneData(const QMimeData &rawData, QStringList formats, const long int *clipboardSequenceNumber) +QVariantMap cloneData(const QMimeData *rawData, const QStringList &formats, const long int *clipboardSequenceNumber) { ClipboardDataGuard data(rawData, clipboardSequenceNumber); return cloneData(data, formats); } -QVariantMap cloneData(const QMimeData &rawData) +QVariantMap cloneData(const QMimeData *rawData) { ClipboardDataGuard data(rawData); diff --git a/src/common/common.h b/src/common/common.h index e859ab3d79..2c78c04490 100644 --- a/src/common/common.h +++ b/src/common/common.h @@ -7,6 +7,7 @@ #include #include +class ClipboardDataGuard; class QByteArray; class QDropEvent; class QFont; @@ -20,10 +21,11 @@ bool isMainThread(); QByteArray makeClipboardOwnerData(); /** Clone data for given formats (text or HTML will be UTF8 encoded). */ -QVariantMap cloneData(const QMimeData &data, QStringList formats, const long int *clipboardSequenceNumber = nullptr); +QVariantMap cloneData(ClipboardDataGuard &data, const QStringList &formats); +QVariantMap cloneData(const QMimeData *data, const QStringList &formats, const long int *clipboardSequenceNumber = nullptr); /** Clone all data as is. */ -QVariantMap cloneData(const QMimeData &data); +QVariantMap cloneData(const QMimeData *data); QString cloneText(const QMimeData &data); diff --git a/src/gui/clipboardbrowser.cpp b/src/gui/clipboardbrowser.cpp index 3c1d81998c..714ea62a07 100644 --- a/src/gui/clipboardbrowser.cpp +++ b/src/gui/clipboardbrowser.cpp @@ -1019,7 +1019,7 @@ void ClipboardBrowser::dropEvent(QDropEvent *event) if (event->dropAction() == Qt::MoveAction && event->source() == this) return; // handled in mouseMoveEvent() - const QVariantMap data = cloneData( *event->mimeData() ); + const QVariantMap data = cloneData( event->mimeData() ); addAndSelect(data, m_dragTargetRow); m_dragTargetRow = -1; } diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index 7090165d4f..f3e15b7fb8 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -1126,7 +1126,7 @@ void MainWindow::onClipboardCommandActionTriggered(CommandAction *commandAction, if (data == nullptr) return; - auto actionData = cloneData(*data); + auto actionData = cloneData(data); if ( !triggeredShortcut.isEmpty() ) actionData.insert(mimeShortcut, triggeredShortcut); @@ -1141,7 +1141,7 @@ void MainWindow::onTabWidgetDropItems(const QString &tabName, const QMimeData *d if (browser) { const QVariantMap dataMap = data->hasFormat(mimeItems) - ? cloneData(*data, QStringList() << mimeItems) : cloneData(*data); + ? cloneData(data, QStringList() << mimeItems) : cloneData(data); browser->addAndSelect(dataMap, 0); } } @@ -3818,7 +3818,7 @@ void MainWindow::pasteItems() QModelIndexList list = c->selectionModel()->selectedIndexes(); std::sort( list.begin(), list.end() ); const int row = list.isEmpty() ? 0 : list.first().row(); - c->addAndSelect( cloneData(*data), row ); + c->addAndSelect( cloneData(data), row ); } void MainWindow::copyItems() diff --git a/src/platform/dummy/dummyclipboard.cpp b/src/platform/dummy/dummyclipboard.cpp index 72babb10f0..3557b003d4 100644 --- a/src/platform/dummy/dummyclipboard.cpp +++ b/src/platform/dummy/dummyclipboard.cpp @@ -36,7 +36,7 @@ QVariantMap DummyClipboard::data(ClipboardMode mode, const QStringList &formats) return {}; const bool isDataSecret = isHidden(*data); - QVariantMap dataMap = cloneData(*data, formats, clipboardSequenceNumber(mode)); + QVariantMap dataMap = cloneData(data, formats, clipboardSequenceNumber(mode)); if (isDataSecret) dataMap[mimeSecret] = QByteArrayLiteral("1"); diff --git a/src/platform/x11/x11platformclipboard.cpp b/src/platform/x11/x11platformclipboard.cpp index 71a8d5c25a..5c7a915775 100644 --- a/src/platform/x11/x11platformclipboard.cpp +++ b/src/platform/x11/x11platformclipboard.cpp @@ -6,6 +6,7 @@ #include "x11info.h" +#include "common/clipboarddataguard.h" #include "common/common.h" #include "common/mimetypes.h" #include "common/log.h" @@ -242,10 +243,10 @@ void X11PlatformClipboard::updateClipboardData(X11PlatformClipboard::ClipboardDa return; } - const QPointer data( mimeData(clipboardData->mode) ); + ClipboardDataGuard data( mimeData(clipboardData->mode), &clipboardData->sequenceNumber ); // Retry to retrieve clipboard data few times. - if (!data) { + if (data.isExpired()) { if ( !X11Info::isPlatformX11() ) return; @@ -266,7 +267,7 @@ void X11PlatformClipboard::updateClipboardData(X11PlatformClipboard::ClipboardDa } clipboardData->retry = 0; - const QByteArray newDataTimestampData = data->data(QStringLiteral("TIMESTAMP")); + const QByteArray newDataTimestampData = data.data(QStringLiteral("TIMESTAMP")); quint32 newDataTimestamp = 0; if ( !newDataTimestampData.isEmpty() ) { QDataStream stream(newDataTimestampData); @@ -279,20 +280,20 @@ void X11PlatformClipboard::updateClipboardData(X11PlatformClipboard::ClipboardDa // In case there is a valid timestamp, omit update if the timestamp and // text did not change. if ( newDataTimestamp != 0 && clipboardData->newDataTimestamp == newDataTimestamp ) { - const QVariantMap newData = cloneData(*data, {mimeText}); - if (!data || newData.value(mimeText) == clipboardData->newData.value(mimeText)) + const QVariantMap newData = cloneData(data, {mimeText}); + if (data.isExpired() || newData.value(mimeText) == clipboardData->newData.value(mimeText)) return; } clipboardData->timerEmitChange.stop(); clipboardData->cloningData = true; - const bool isDataSecret = isHidden(*data); - const auto sequenceNumberOrig = clipboardData->sequenceNumber; - clipboardData->newData = cloneData(*data, clipboardData->formats, &clipboardData->sequenceNumber); + const bool isDataSecret = isHidden(*data.mimeData()); + clipboardData->newData = cloneData(data, clipboardData->formats); if (isDataSecret) clipboardData->newData[mimeSecret] = QByteArrayLiteral("1"); clipboardData->cloningData = false; - if (sequenceNumberOrig != clipboardData->sequenceNumber) { + + if (data.isExpired()) { m_timerCheckAgain.setInterval(0); m_timerCheckAgain.start(); return; diff --git a/src/scriptable/scriptableproxy.cpp b/src/scriptable/scriptableproxy.cpp index dc94c6dd4a..937878d106 100644 --- a/src/scriptable/scriptableproxy.cpp +++ b/src/scriptable/scriptableproxy.cpp @@ -2549,7 +2549,7 @@ QByteArray ScriptableProxy::getClipboardData(const QString &mime, ClipboardMode if (mime == "?") return data->formats().join("\n").toUtf8() + '\n'; - return cloneData(*data, QStringList(mime)).value(mime).toByteArray(); + return cloneData(data, QStringList(mime)).value(mime).toByteArray(); } bool ScriptableProxy::hasClipboardFormat(const QString &mime, ClipboardMode mode)