QtPass Developer documentation
QtPass test coverage
Current view: top level - src/clipboardmanager.cpp (source / functions) Coverage Total Hit
Test: QtPass Lines: 98.9 % 89 88
Test Date: 2026-11-14 13:19:18 Functions: 100.0 % 10 10

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
       2              : // SPDX-License-Identifier: GPL-3.0-or-later
       3              : /**
       4              :  * @file
       5              :  * @brief Implements ClipboardManager and the per-platform clipboard hints that
       6              :  * keep secrets out of clipboard histories.
       7              :  */
       8              : #include "clipboardmanager.h"
       9              : #include "qtpasssettings.h"
      10              : #include "util.h"
      11              : #include <QApplication>
      12              : #include <QClipboard>
      13              : #include <utility>
      14              : 
      15          156 : ClipboardManager::ClipboardManager(QObject *parent) : QObject(parent) {
      16          156 :   setAutoclearTimer();
      17          156 :   m_clearTimer.setSingleShot(true);
      18          156 :   connect(&m_clearTimer, &QTimer::timeout, this, &ClipboardManager::clear);
      19          156 :   connect(QApplication::instance(), &QApplication::aboutToQuit, this,
      20          156 :           &ClipboardManager::clear);
      21          156 : }
      22              : 
      23          160 : void ClipboardManager::setAutoclearTimer() {
      24          320 :   m_clearTimer.setInterval(MS_PER_SECOND *
      25          320 :                            QtPassSettings::getAutoclearSeconds());
      26          160 : }
      27              : 
      28           18 : void ClipboardManager::copyIfAlways(const QString &text,
      29              :                                     const QString &p_output) {
      30           18 :   const AppSettings s = QtPassSettings::load();
      31              :   // Only "always copy" mode actually places text on the clipboard here; in
      32              :   // on-demand mode the copy happens later from the field's copy button.
      33              :   // Crucially, do not touch m_clippedText otherwise — it tracks what is
      34              :   // currently on the clipboard so the autoclear timer knows what to clear.
      35              :   // Overwriting it when merely showing another entry left a previously copied
      36              :   // password on the clipboard past its autoclear window (#1607).
      37           18 :   if (s.clipBoardType == Enums::CLIPBOARD_ALWAYS && !p_output.isEmpty()) {
      38            2 :     copyText(text);
      39              :   }
      40           18 : }
      41              : 
      42           31 : void ClipboardManager::copyText(const QString &text) {
      43           31 :   const AppSettings s = QtPassSettings::load();
      44           31 :   QClipboard *clip = QApplication::clipboard();
      45              : 
      46              :   QClipboard::Mode mode = QClipboard::Clipboard;
      47           31 :   if (s.useSelection && clip->supportsSelection()) {
      48              :     mode = QClipboard::Selection;
      49              :   }
      50              : 
      51              :   // What was there before the first secret, to put back when autoclear
      52              :   // runs (as pass -c does). Text only, as pass keeps it: rich content comes
      53              :   // back as its plain text. Where Qt cannot read the clipboard without focus
      54              :   // (some Wayland compositors, a copy from the tray), "before" reads as
      55              :   // nothing and autoclear clears instead: the safe way round.
      56           31 :   if (s.useAutoclear && s.restoreClipboard) {
      57           17 :     std::optional<QString> &previous = mode == QClipboard::Selection
      58              :                                            ? m_previousSelection
      59              :                                            : m_previousClipboard;
      60           17 :     const QString now = clip->text(mode);
      61              :     // Taken once per window, at the first copy, whatever is there: a text
      62              :     // of ours, or the very text being copied, is saved as nothing (restoring
      63              :     // it would leave a secret on the clipboard for good), and a later copy
      64              :     // in the window cannot take a snapshot instead.
      65           17 :     if (!previous) {
      66              :       const bool safe =
      67           11 :           now != text && now != m_clippedText && !m_otherCopies.contains(now);
      68           22 :       previous = safe ? now : QString();
      69              :     }
      70              :   }
      71           31 :   forgetIfSaved(text);
      72           31 :   clip->setMimeData(buildClipboardMimeData(text), mode);
      73              : 
      74           31 :   m_clippedText = text;
      75           31 :   emit statusMessage(tr("Copied to clipboard"));
      76           31 :   if (s.useAutoclear) {
      77           21 :     m_clearTimer.start();
      78              :   }
      79           31 : }
      80              : 
      81           82 : void ClipboardManager::forgetIfSaved(const QString &text) {
      82              :   // The saved text is copied as a secret now: putting it back would leave
      83              :   // that secret on the clipboard. Emptied, not dropped, so no later copy
      84              :   // takes a new snapshot in this window either; clear() then clears.
      85          183 :   for (std::optional<QString> *previous :
      86          246 :        {&m_previousClipboard, &m_previousSelection}) {
      87          164 :     if (*previous && **previous == text) {
      88            4 :       *previous = QString();
      89              :     }
      90              :   }
      91           82 : }
      92              : 
      93           51 : void ClipboardManager::trackCopied(const QString &text) {
      94           51 :   if (text.isEmpty()) {
      95              :     return;
      96              :   }
      97           51 :   forgetIfSaved(text);
      98              :   // Kept beside m_clippedText, not instead of it: replacing it would leave a
      99              :   // password copied earlier on the clipboard past its autoclear (#1607).
     100              :   constexpr qsizetype kMaxOtherCopies = 16;
     101           51 :   if (text != m_clippedText && !m_otherCopies.contains(text)) {
     102              :     // A mouse selection reports itself on every change while it grows or
     103              :     // shrinks (the X11 primary selection); only where it ends counts, or a
     104              :     // passing "a" would stay tracked and autoclear could take someone
     105              :     // else's "a" later.
     106              :     // An earlier copy still on the clipboard proper (Ctrl+C) stays tracked.
     107              :     const QString &last =
     108           51 :         m_otherCopies.isEmpty() ? QString() : m_otherCopies.last();
     109          138 :     if (!last.isEmpty() && (text.contains(last) || last.contains(text)) &&
     110           54 :         QApplication::clipboard()->text(QClipboard::Clipboard) != last) {
     111            2 :       m_otherCopies.last() = text;
     112              :     } else {
     113           49 :       if (m_otherCopies.size() >= kMaxOtherCopies) {
     114              :         // Make room, but never by forgetting a text that is still on the
     115              :         // clipboard or in the selection: it would then outlive autoclear.
     116              :         // At most two are, so the oldest of the rest can always go.
     117              :         const QString onClipboard =
     118           25 :             QApplication::clipboard()->text(QClipboard::Clipboard);
     119              :         const QString inSelection =
     120           25 :             QApplication::clipboard()->text(QClipboard::Selection);
     121           50 :         for (qsizetype i = 0; i < m_otherCopies.size(); ++i) {
     122           50 :           if (m_otherCopies.at(i) != onClipboard &&
     123              :               m_otherCopies.at(i) != inSelection) {
     124              :             m_otherCopies.removeAt(i);
     125              :             break;
     126              :           }
     127              :         }
     128              :       }
     129              :       m_otherCopies.append(text);
     130              :     }
     131              :   }
     132           51 :   if (QtPassSettings::load().useAutoclear) {
     133           51 :     m_clearTimer.start();
     134              :   }
     135              : }
     136              : 
     137           34 : void ClipboardManager::clear() {
     138              :   // Whatever happens below, the saved text is forgotten: it is the user's,
     139              :   // and no later autoclear may put it back.
     140              :   const std::optional<QString> previousClipboard =
     141           34 :       std::exchange(m_previousClipboard, std::nullopt);
     142              :   const std::optional<QString> previousSelection =
     143           34 :       std::exchange(m_previousSelection, std::nullopt);
     144           34 :   if (m_clippedText.isEmpty() && m_otherCopies.isEmpty()) {
     145              :     // Nothing of ours is out there. The old code compared the empty tracker
     146              :     // with an empty selection, "cleared" that and said so on every deselect.
     147              :     return;
     148              :   }
     149           38 :   const auto ours = [this](const QString &text) {
     150           38 :     return !text.isEmpty() &&
     151           19 :            (text == m_clippedText || m_otherCopies.contains(text));
     152           19 :   };
     153           19 :   QClipboard *clipboard = QApplication::clipboard();
     154           19 :   const bool restore = QtPassSettings::load().restoreClipboard;
     155           19 :   bool cleared = false;
     156           19 :   bool restored = false;
     157              :   // Ours still there: the text from before comes back if there was one and
     158              :   // what is there is copyText()'s own copy, otherwise it is cleared. A copy
     159              :   // from a shown field (trackCopied()) is cleared on purpose, not restored
     160              :   // over: it can come long after copyText(), when "before" is stale, and the
     161              :   // user chose that copy themselves. Something else copied meanwhile is left
     162              :   // alone. The setting is read now: switched off during the window, the
     163              :   // saved text was still held until here, and is dropped unused.
     164           38 :   const auto putBack = [&](QClipboard::Mode mode,
     165              :                            const std::optional<QString> &previous) {
     166           38 :     const QString now = clipboard->text(mode);
     167           38 :     if (!ours(now)) {
     168              :       return;
     169              :     }
     170           16 :     if (restore && previous && !previous->isEmpty() && now == m_clippedText) {
     171            3 :       clipboard->setText(*previous, mode);
     172            3 :       restored = true;
     173            3 :       return;
     174              :     }
     175           13 :     clipboard->clear(mode);
     176           13 :     if (mode == QClipboard::Selection) {
     177            0 :       clipboard->setText(QString(""), QClipboard::Selection);
     178              :     }
     179           13 :     cleared = true;
     180           19 :   };
     181           19 :   putBack(QClipboard::Selection, previousSelection);
     182           19 :   putBack(QClipboard::Clipboard, previousClipboard);
     183           38 :   emit statusMessage(restored  ? tr("Clipboard restored")
     184           16 :                      : cleared ? tr("Clipboard cleared")
     185              :                                : tr("Clipboard not cleared"));
     186           19 :   m_clippedText.clear();
     187           19 :   m_otherCopies.clear();
     188              : }
     189              : 
     190           51 : auto buildClipboardMimeData(const QString &text) -> QMimeData * {
     191           51 :   auto *mimeData = new QMimeData();
     192           51 :   mimeData->setText(text);
     193              : // The KDE hint (what wl-copy --sensitive sets) keeps the secret out of
     194              : // Klipper, CopyQ and cliphist history, on the BSDs as well as on Linux.
     195              : #if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
     196          102 :   mimeData->setData("x-kde-passwordManagerHint", QByteArray("secret"));
     197              : #endif
     198              : #ifdef Q_OS_MAC
     199              :   mimeData->setData("application/x-nspasteboard-concealed-type", QByteArray());
     200              : #endif
     201              : #ifdef Q_OS_WIN
     202              :   mimeData->setData("ExcludeClipboardContentFromMonitorProcessing",
     203              :                     dwordBytes(1));
     204              :   mimeData->setData("CanIncludeInClipboardHistory", dwordBytes(0));
     205              :   mimeData->setData("CanUploadToCloudClipboard", dwordBytes(0));
     206              : #endif
     207           51 :   return mimeData;
     208              : }
        

Generated by: LCOV version 2.0-1