• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IJHack / QtPass / 25731493901

12 May 2026 11:28AM UTC coverage: 28.416% (+0.2%) from 28.263%
25731493901

push

github

web-flow
fix(security): path-traversal hardening in new-file/rename + drag/drop (#1464)

User-typed names in MainWindow's "new file" / "new folder" / "rename"
input dialogs were concatenated with the current store-relative directory
and passed onward without validation. A user could type
"../../etc/passwd" (or paste an absolute path) and QtPass would happily
create / move / rename outside the password store via GPG encryption.

Symmetric weakness in StoreModel::executeDropAction: the encoded mime
payload was trusted, and there was no check that the resolved source +
destination paths stayed inside the store. A crafted drop or a symlink
inside the store pointing outside (e.g. into ~/.ssh) would escape.

Fix:

- Util::isPathInStore(storeRoot, candidate): canonicalises the candidate
  via QFileInfo::canonicalFilePath() for existing targets, or canonicalises
  the nearest existing ancestor and re-appends the leaf for not-yet-created
  paths. Returns true iff the result is equal to or strictly inside the
  canonicalised store root. Catches `..` escapes, absolute escapes, and
  symlink-out from inside the store.

- MainWindow::confirmPathInStore(): wraps Util::isPathInStore and shows a
  non-blocking "Invalid name" warning before bailing. Called from
  addPassword, addFolder, renameFolder, renamePassword before the Insert /
  mkdir / Move call.

- StoreModel::executeDropAction(): rejects (returns false) any drop whose
  source or destination resolves outside the store, logging a warning.
  Both endpoints are validated; final move destination is constructed
  from a canonical-inside-store base plus a leaf segment (QFileInfo's
  fileName() returns just the last path component), so the constructed
  target is always inside the store.

Tests (tst_util): 6 new cases — happy path, `..` escape, absolute path
escape, symlink-out escape (skipped on Windows where link creation needs
elevation), allows-new-child for the create flow, and empty-args edge
cases.

Build clean, 119... (continued)

23 of 38 new or added lines in 3 files covered. (60.53%)

14 existing lines in 3 files now uncovered.

1909 of 6718 relevant lines covered (28.42%)

26.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/mainwindow.cpp
1
// SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "mainwindow.h"
4

5
#ifdef QT_DEBUG
6
#include "debughelper.h"
7
#endif
8

9
#include "configdialog.h"
10
#include "enums.h"
11
#include "executor.h"
12
#include "exportpublickeydialog.h"
13
#include "filecontent.h"
14
#include "passworddialog.h"
15
#include "qpushbuttonasqrcode.h"
16
#include "qpushbuttonshowpassword.h"
17
#include "qpushbuttonwithclipboard.h"
18
#include "qtpass.h"
19
#include "qtpasssettings.h"
20
#include "trayicon.h"
21
#include "ui_mainwindow.h"
22
#include "usersdialog.h"
23
#include "util.h"
24
#include <QApplication>
25
#include <QCloseEvent>
26
#include <QDesktopServices>
27
#include <QDialog>
28
#include <QDirIterator>
29
#include <QDockWidget>
30
#include <QFileInfo>
31
#include <QHBoxLayout>
32
#include <QInputDialog>
33
#include <QLabel>
34
#include <QLineEdit>
35
#include <QMenu>
36
#include <QMessageBox>
37
#include <QScrollBar>
38
#include <QShortcut>
39
#include <QTextCursor>
40
#include <QTextEdit>
41
#include <QTimer>
42
#include <QToolButton>
43
#include <QTreeWidget>
44
#include <utility>
45

46
/**
47
 * @brief MainWindow::MainWindow handles all of the main functionality and also
48
 * the main window.
49
 * @param searchText for searching from cli
50
 * @param parent pointer
51
 */
52
MainWindow::MainWindow(const QString &searchText, QWidget *parent)
×
53
    : QMainWindow(parent), ui(new Ui::MainWindow) {
×
54
#ifdef __APPLE__
55
  // extra treatment for mac os
56
  // see http://doc.qt.io/qt-5/qkeysequence.html#qt_set_sequence_auto_mnemonic
57
  qt_set_sequence_auto_mnemonic(true);
58
#endif
59
  ui->setupUi(this);
×
60

61
  m_qtPass = new QtPass(this);
×
62

63
  // register shortcut ctrl/cmd + Q to close the main window
64
  new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q), this, SLOT(close()));
×
65
  // register shortcut ctrl/cmd + C to copy the currently selected password
66
  new QShortcut(QKeySequence(QKeySequence::StandardKey::Copy), this,
×
67
                SLOT(copyPasswordFromTreeview()));
×
68

69
  model.setNameFilters(QStringList() << "*.gpg");
×
70
  model.setNameFilterDisables(false);
×
71

72
  /*
73
   * I added this to solve Windows bug but now on GNU/Linux the main folder,
74
   * if hidden, disappear
75
   *
76
   * model.setFilter(QDir::NoDot);
77
   */
78

79
  QString passStore = QtPassSettings::getPassStore(Util::findPasswordStore());
×
80

81
  QModelIndex rootDir = model.setRootPath(passStore);
×
82
  model.fetchMore(rootDir);
×
83

84
  proxyModel.setModelAndStore(&model, passStore);
×
85
  selectionModel.reset(new QItemSelectionModel(&proxyModel));
×
86

87
  ui->treeView->setModel(&proxyModel);
×
88
  ui->treeView->setRootIndex(proxyModel.mapFromSource(rootDir));
×
89
  ui->treeView->setColumnHidden(1, true);
×
90
  ui->treeView->setColumnHidden(2, true);
×
91
  ui->treeView->setColumnHidden(3, true);
×
92
  ui->treeView->setHeaderHidden(true);
93
  ui->treeView->setIndentation(15);
×
94
  ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
×
95
  ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu);
×
96
  ui->treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch);
×
97
  ui->treeView->sortByColumn(0, Qt::AscendingOrder);
×
98
  connect(ui->treeView, &QWidget::customContextMenuRequested, this,
×
99
          &MainWindow::showContextMenu);
×
100
  connect(ui->treeView, &DeselectableTreeView::emptyClicked, this,
×
101
          &MainWindow::deselect);
×
102

103
  if (QtPassSettings::isUseMonospace()) {
×
104
    QFont monospace("Monospace");
×
105
    monospace.setStyleHint(QFont::Monospace);
×
106
    ui->textBrowser->setFont(monospace);
×
107
  }
×
108
  if (QtPassSettings::isNoLineWrapping()) {
×
109
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
110
  }
111
  ui->textBrowser->setOpenExternalLinks(true);
×
112
  ui->textBrowser->setContextMenuPolicy(Qt::CustomContextMenu);
×
113
  connect(ui->textBrowser, &QWidget::customContextMenuRequested, this,
×
114
          &MainWindow::showBrowserContextMenu);
×
115

116
  updateProfileBox();
×
117

118
  QtPassSettings::getPass()->updateEnv();
×
119
  clearPanelTimer.setInterval(MS_PER_SECOND *
×
120
                              QtPassSettings::getAutoclearPanelSeconds());
×
121
  clearPanelTimer.setSingleShot(true);
×
122
  connect(&clearPanelTimer, &QTimer::timeout, this, [this]() { clearPanel(); });
×
123

124
  searchTimer.setInterval(350);
×
125
  searchTimer.setSingleShot(true);
×
126

127
  connect(&searchTimer, &QTimer::timeout, this, &MainWindow::onTimeoutSearch);
×
128

129
  initToolBarButtons();
×
130
  initStatusBar();
×
131
  initProcessOutputPanel();
×
132

133
  connect(QtPassSettings::getPass(), &Pass::finishedAnyWithPid, this,
×
134
          [this](const QString &out, const QString &err, Enums::PROCESS pid) {
×
135
            // Never route potentially-secret output through the panel:
136
            // - PASS_SHOW / PASS_OTP_GENERATE go via dedicated signals to
137
            //   the main text browser (which clears on a timer).
138
            // - PASS_GREP returns lines from password files; #252 must
139
            //   not leak those into a long-lived panel.
140
            // - PASS_INSERT's stdin is the password; stdout normally
141
            //   carries gpg/git progress only, but exclude defensively
142
            //   in case a future code path uses --echo or similar.
143
            if (isSensitiveProcess(pid)) {
×
144
              return;
145
            }
146
            if (!out.isEmpty()) {
×
147
              onProcessOutput(out, false, pid);
×
148
            }
149
            if (!err.isEmpty()) {
×
150
              onProcessOutput(err, true, pid);
×
151
            }
152
          });
153

154
  ui->lineEdit->setClearButtonEnabled(true);
×
155
  updateGrepButtonVisibility();
×
156

157
  setUiElementsEnabled(true);
×
158

159
  ui->lineEdit->setText(searchText);
×
160

161
  if (!m_qtPass->init()) {
×
162
    // no working config so this should just quit
163
    QApplication::quit();
×
164
    return;
165
  }
166

167
  // Initial focus is handled in showEvent() once the window is actually
168
  // mapped. Scheduling it here via a 10 ms QTimer was racy: if the timer
169
  // fires while the window has not yet been realised — e.g. an
170
  // ActivationChange queued by main()'s `activateWindow()` call before
171
  // `show()`, or a nested QDialog::exec() inside init() — the
172
  // QLineEdit's internal text engine hasn't been wired up and
173
  // selectAll() segfaults inside Qt (see #1187, #1188).
174
}
×
175

176
MainWindow::~MainWindow() { delete m_qtPass; }
×
177

178
/**
179
 * @brief MainWindow::focusInput selects any text (if applicable) in the search
180
 * box and sets focus to it. Allows for easy searching, called at application
181
 * start and when receiving empty message in MainWindow::messageAvailable when
182
 * compiled with SINGLE_APP=1 (default).
183
 */
184
void MainWindow::focusInput() {
×
185
  // Resolve the QLineEdit through the live widget tree rather than the
186
  // cached `ui->lineEdit` pointer.
187
  //
188
  // On a fresh-config first launch the constructor calls
189
  // `m_qtPass->init()` → `MainWindow::config()`, and `config()`'s
190
  // `applyWindowFlagsSettings()` does `setWindowFlags(...)` + `show()`
191
  // on the main window. `setWindowFlags` on a top-level widget rebuilds
192
  // the native window via `setParent(nullptr, flags)`; under Qt 6.11
193
  // we observed the QLineEdit attached to the centralWidget gets
194
  // destroyed in that rebuild while `ui->lineEdit` still holds its old
195
  // address — leading to a SIGSEGV inside `QWidget::testAttribute`
196
  // (called from `QLineEdit::isVisible` / `selectAll`). `findChild<>()`
197
  // walks the current hierarchy and returns null cleanly when the
198
  // widget is gone, so `focusInput` becomes a safe no-op instead of a
199
  // use-after-free.
200
  if (!isVisible()) {
×
201
    return;
202
  }
203
  auto *lineEdit = findChild<QLineEdit *>(QStringLiteral("lineEdit"));
×
204
  if (lineEdit == nullptr || !lineEdit->isVisible()) {
×
205
    return;
206
  }
207
  lineEdit->selectAll();
×
208
  lineEdit->setFocus();
×
209
  // Only mark the first-show focus pulse as done once it's actually
210
  // landed; setting it eagerly in showEvent() would consume the
211
  // one-shot if focusInput returned early (mid-rebuild widget state)
212
  // and we'd never retry.
213
  m_initialShowDone = true;
×
214
}
215

216
/**
217
 * @brief MainWindow::changeEvent sets focus to the search box
218
 * @param event
219
 */
220
void MainWindow::changeEvent(QEvent *event) {
×
221
  QWidget::changeEvent(event);
×
222
  if (event->type() == QEvent::ActivationChange && isActiveWindow() &&
×
223
      isVisible()) {
224
    // Defer one event-loop tick so the synchronous activation dispatch
225
    // chain (`QApplicationPrivate::setActiveWindow` → `notify_helper`)
226
    // unwinds before we touch widget state — calling `focusInput()`
227
    // inline from this stack has segfaulted in past iterations because
228
    // mid-rebuild ui state isn't fully wired up yet.
229
    QMetaObject::invokeMethod(this, &MainWindow::focusInput,
×
230
                              Qt::QueuedConnection);
231
  }
232
}
×
233

234
/**
235
 * @brief First-show hook: run the initial focusInput() pulse once the
236
 *        window is actually mapped. The widget's internal data is fully
237
 *        initialised by this point, so QLineEdit::selectAll() is safe.
238
 * @param event Show event passed to the base class.
239
 */
240
void MainWindow::showEvent(QShowEvent *event) {
×
241
  QMainWindow::showEvent(event);
×
242
  if (m_initialShowDone) {
×
243
    return;
244
  }
245
  // Queue the focus pulse for the next event-loop tick so the platform
246
  // map round-trip and any pending widget rebuilds (e.g. setWindowFlags
247
  // from the config wizard path) settle before we look up the line
248
  // edit. The `m_initialShowDone` latch is set inside focusInput()
249
  // *after* it actually focuses, so a transient failed lookup just
250
  // re-queues on the next show rather than silently dropping.
251
  QMetaObject::invokeMethod(this, &MainWindow::focusInput,
×
252
                            Qt::QueuedConnection);
253
}
254

255
/**
256
 * @brief MainWindow::initToolBarButtons init main ToolBar and connect actions
257
 */
258
void MainWindow::initToolBarButtons() {
×
259
  connect(ui->actionAddPassword, &QAction::triggered, this,
×
260
          &MainWindow::addPassword);
×
261
  connect(ui->actionAddFolder, &QAction::triggered, this,
×
262
          &MainWindow::addFolder);
×
263
  connect(ui->actionEdit, &QAction::triggered, this, &MainWindow::onEdit);
×
264
  connect(ui->actionDelete, &QAction::triggered, this, &MainWindow::onDelete);
×
265
  connect(ui->actionPush, &QAction::triggered, this, &MainWindow::onPush);
×
266
  connect(ui->actionUpdate, &QAction::triggered, this, &MainWindow::onUpdate);
×
267
  connect(ui->actionUsers, &QAction::triggered, this, &MainWindow::onUsers);
×
268
  connect(ui->actionConfig, &QAction::triggered, this, &MainWindow::onConfig);
×
269
  connect(ui->actionOtp, &QAction::triggered, this, &MainWindow::onOtp);
×
270

271
  ui->actionAddPassword->setIcon(
×
272
      QIcon::fromTheme("document-new", QIcon(":/icons/document-new.svg")));
×
273
  ui->actionAddFolder->setIcon(
×
274
      QIcon::fromTheme("folder-new", QIcon(":/icons/folder-new.svg")));
×
275
  ui->actionEdit->setIcon(QIcon::fromTheme(
×
276
      "document-properties", QIcon(":/icons/document-properties.svg")));
×
277
  ui->actionDelete->setIcon(
×
278
      QIcon::fromTheme("edit-delete", QIcon(":/icons/edit-delete.svg")));
×
279
  ui->actionPush->setIcon(
×
280
      QIcon::fromTheme("go-up", QIcon(":/icons/go-top.svg")));
×
281
  ui->actionUpdate->setIcon(
×
282
      QIcon::fromTheme("go-down", QIcon(":/icons/go-bottom.svg")));
×
283
  ui->actionUsers->setIcon(QIcon::fromTheme(
×
284
      "x-office-address-book", QIcon(":/icons/x-office-address-book.svg")));
×
285
  ui->actionConfig->setIcon(QIcon::fromTheme(
×
286
      "applications-system", QIcon(":/icons/applications-system.svg")));
×
287
}
×
288

289
/**
290
 * @brief MainWindow::initStatusBar init statusBar with default message and logo
291
 */
292
void MainWindow::initStatusBar() {
×
293
  ui->statusBar->showMessage(tr("Welcome to QtPass %1").arg(VERSION), 2000);
×
294

295
  QPixmap logo = QPixmap::fromImage(QImage(":/artwork/icon.svg"))
×
296
                     .scaledToHeight(statusBar()->height());
×
297
  auto *logoApp = new QLabel(statusBar());
×
298
  logoApp->setPixmap(logo);
×
299
  statusBar()->addPermanentWidget(logoApp);
×
300
}
×
301

302
/**
303
 * @brief Build the process-output panel as a bottom QDockWidget.
304
 *
305
 * The panel is constructed programmatically rather than declared in
306
 * mainwindow.ui: uic only places QMainWindow's top-level children into
307
 * the centralWidget / statusBar / menuBar / toolBars / dock-widget
308
 * slots, and the previous home (statusBar()->addPermanentWidget()) made
309
 * an 80–150 px tall QTextEdit sit inside what is otherwise a thin
310
 * status row. A QDockWidget at the bottom dock area is the conventional
311
 * place for an IDE-style output console, and it gives users
312
 * detach/move for free.
313
 */
314
void MainWindow::initProcessOutputPanel() {
×
315
  m_processOutputWidget = new QWidget;
×
316
  m_processOutputWidget->setObjectName(QStringLiteral("processOutputWidget"));
×
317
  auto *outputLayout = new QHBoxLayout(m_processOutputWidget);
×
318
  outputLayout->setObjectName(QStringLiteral("processOutputLayout"));
×
319
  outputLayout->setContentsMargins(0, 0, 0, 0);
×
320
  m_clearOutputButton = new QToolButton(m_processOutputWidget);
×
321
  m_clearOutputButton->setObjectName(QStringLiteral("clearOutputButton"));
×
322
  m_clearOutputButton->setText(tr("Clear"));
×
323
  m_clearOutputButton->setToolTip(tr("Clear output"));
×
324
  outputLayout->addWidget(m_clearOutputButton);
×
325
  m_processOutputEdit = new QTextEdit(m_processOutputWidget);
×
326
  m_processOutputEdit->setObjectName(QStringLiteral("processOutputEdit"));
×
327
  m_processOutputEdit->setReadOnly(true);
×
328
  m_processOutputEdit->setAcceptRichText(false);
×
329
  outputLayout->addWidget(m_processOutputEdit);
×
330

331
  m_processOutputDock = new QDockWidget(tr("Process Output"), this);
×
332
  m_processOutputDock->setObjectName(QStringLiteral("processOutputDock"));
×
333
  m_processOutputDock->setFeatures(QDockWidget::DockWidgetMovable |
×
334
                                   QDockWidget::DockWidgetFloatable);
335
  m_processOutputDock->setAllowedAreas(Qt::BottomDockWidgetArea |
×
336
                                       Qt::TopDockWidgetArea);
337
  m_processOutputDock->setWidget(m_processOutputWidget);
×
338
  addDockWidget(Qt::BottomDockWidgetArea, m_processOutputDock);
×
339
  // setVisible after addDockWidget so our explicit preference wins
340
  // even if QMainWindow applies any cached state when the dock is
341
  // attached. restoreWindow() runs before this method (it's called
342
  // from the QtPass ctor, which is constructed at the top of the
343
  // MainWindow ctor), so the saved layout has already been processed
344
  // by the time we get here.
345
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
×
346

347
  connect(m_clearOutputButton, &QToolButton::clicked, this,
×
348
          &MainWindow::on_clearOutputButton_clicked);
×
349

350
  // Hysteresis: while the user is actively dragging the slider, don't
351
  // touch m_autoScroll on every tick — a brief overshoot at maximum
352
  // would silently re-arm auto-scroll without an explicit release. Only
353
  // commit on slider release. Wheel/keyboard scroll never sets
354
  // isSliderDown(), so they still update immediately.
355
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::valueChanged,
×
356
          this, [this]() {
×
357
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
358
            if (sb->isSliderDown())
×
359
              return;
360
            m_autoScroll = sb->value() >= sb->maximum();
×
361
          });
362
  connect(m_processOutputEdit->verticalScrollBar(), &QScrollBar::sliderReleased,
×
363
          this, [this]() {
×
364
            auto *sb = m_processOutputEdit->verticalScrollBar();
×
365
            m_autoScroll = sb->value() >= sb->maximum();
×
366
          });
×
367
}
×
368

369
auto MainWindow::getCurrentTreeViewIndex() -> QModelIndex {
×
370
  return ui->treeView->currentIndex();
×
371
}
372

373
void MainWindow::cleanKeygenDialog() {
×
374
  if (m_keygenDialog != nullptr) {
×
375
    m_keygenDialog->close();
×
376
  }
377
  m_keygenDialog = nullptr;
×
378
}
×
379

380
/**
381
 * @brief Displays the given text in the main window text browser, optionally
382
 * marking it as an error and/or rendering it as HTML.
383
 * @example
384
 * MainWindow window;
385
 * window.flashText("Operation completed.", false, false);
386
 *
387
 * @param const QString &text - The text content to display.
388
 * @param const bool isError - If true, sets the text color to red before
389
 * displaying the text.
390
 * @param const bool isHtml - If true, treats the text as HTML and appends it to
391
 * the existing HTML content.
392
 * @return void - No return value.
393
 */
394
void MainWindow::flashText(const QString &text, const bool isError,
×
395
                           const bool isHtml) {
396
  if (isError) {
×
397
    ui->textBrowser->setTextColor(Qt::red);
×
398
  }
399

400
  if (isHtml) {
×
401
    QString _text = text;
402
    if (!ui->textBrowser->toPlainText().isEmpty()) {
×
403
      _text = ui->textBrowser->toHtml() + _text;
×
404
    }
405
    ui->textBrowser->setHtml(_text);
×
406
  } else {
407
    ui->textBrowser->setText(text);
×
408
  }
409
}
×
410

411
/**
412
 * @brief MainWindow::config pops up the configuration screen and handles all
413
 * inter-window communication
414
 */
415
void MainWindow::applyTextBrowserSettings() {
×
416
  if (QtPassSettings::isUseMonospace()) {
×
417
    QFont monospace("Monospace");
×
418
    monospace.setStyleHint(QFont::Monospace);
×
419
    ui->textBrowser->setFont(monospace);
×
420
  } else {
×
421
    ui->textBrowser->setFont(QFont());
×
422
  }
423

424
  if (QtPassSettings::isNoLineWrapping()) {
×
425
    ui->textBrowser->setLineWrapMode(QTextBrowser::NoWrap);
×
426
  } else {
427
    ui->textBrowser->setLineWrapMode(QTextBrowser::WidgetWidth);
×
428
  }
429
}
×
430

431
void MainWindow::applyWindowFlagsSettings() {
×
432
  if (QtPassSettings::isAlwaysOnTop()) {
×
433
    Qt::WindowFlags flags = windowFlags();
434
    this->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
435
  } else {
436
    this->setWindowFlags(Qt::Window);
×
437
  }
438
  this->show();
×
439
}
×
440

441
/**
442
 * @brief Opens and processes the application configuration dialog, then applies
443
 * any accepted settings.
444
 * @example
445
 * config();
446
 *
447
 * @return void - This function does not return a value.
448
 */
449
void MainWindow::config() {
×
450
  QScopedPointer<ConfigDialog> d(new ConfigDialog(this));
×
451
  d->setModal(true);
×
452
  // Automatically default to pass if it's available
453
  if (m_qtPass->isFreshStart() &&
×
454
      QFile(QtPassSettings::getPassExecutable()).exists()) {
×
455
    QtPassSettings::setUsePass(true);
×
456
  }
457

458
  if (m_qtPass->isFreshStart()) {
×
459
    d->wizard(); // run initial setup wizard for first-time configuration
×
460
  }
461
  if (d->exec()) {
×
462
    if (d->result() == QDialog::Accepted) {
×
463
      applyTextBrowserSettings();
×
464
      applyWindowFlagsSettings();
×
465

466
      updateProfileBox();
×
467
      const QString passStore = QtPassSettings::getPassStore();
×
468
      proxyModel.setStore(passStore);
×
469
      ui->treeView->setRootIndex(
×
470
          proxyModel.mapFromSource(model.setRootPath(passStore)));
×
471
      deselect();
×
472
      ui->treeView->setCurrentIndex(QModelIndex());
×
473

474
      if (m_qtPass->isFreshStart() && !Util::configIsValid()) {
×
475
        config();
×
476
      }
477
      QtPassSettings::getPass()->updateEnv();
×
478
      clearPanelTimer.setInterval(MS_PER_SECOND *
×
479
                                  QtPassSettings::getAutoclearPanelSeconds());
×
480
      m_qtPass->setClipboardTimer();
×
481

482
      updateGitButtonVisibility();
×
483
      updateOtpButtonVisibility();
×
484
      updateGrepButtonVisibility();
×
485
      updateProcessOutputVisibility();
×
486
      if (QtPassSettings::isUseTrayIcon() && m_tray == nullptr) {
×
487
        initTrayIcon();
×
488
      } else if (!QtPassSettings::isUseTrayIcon() && m_tray != nullptr) {
×
489
        destroyTrayIcon();
×
490
      }
491
    }
492

493
    m_qtPass->setFreshStart(false);
×
494
  }
495
}
×
496

497
/**
498
 * @brief MainWindow::onUpdate do a git pull
499
 */
500
void MainWindow::onUpdate(bool block) {
×
501
  ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
502
  if (block) {
×
503
    QtPassSettings::getPass()->GitPull_b();
×
504
  } else {
505
    QtPassSettings::getPass()->GitPull();
×
506
  }
507
}
×
508

509
/**
510
 * @brief MainWindow::onPush do a git push
511
 */
512
void MainWindow::onPush() {
×
513
  if (QtPassSettings::isUseGit()) {
×
514
    ui->statusBar->showMessage(tr("Updating password-store"), 2000);
×
515
    QtPassSettings::getPass()->GitPush();
×
516
  }
517
}
×
518

519
/**
520
 * @brief MainWindow::getFile get the selected file path
521
 * @param index
522
 * @param forPass returns relative path without '.gpg' extension
523
 * @return path
524
 * @return
525
 */
526
auto MainWindow::getFile(const QModelIndex &index, bool forPass) -> QString {
×
527
  if (!index.isValid() ||
×
528
      !model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
529
    return {};
530
  }
531
  QString filePath = model.filePath(proxyModel.mapToSource(index));
×
532
  if (forPass) {
×
533
    filePath = QDir(QtPassSettings::getPassStore()).relativeFilePath(filePath);
×
534
    filePath.replace(Util::endsWithGpg(), "");
×
535
  }
536
  return filePath;
537
}
538

539
/**
540
 * @brief MainWindow::on_treeView_clicked read the selected password file
541
 * @param index
542
 */
543
void MainWindow::on_treeView_clicked(const QModelIndex &index) {
×
544
  bool cleared = ui->treeView->currentIndex().flags() == Qt::NoItemFlags;
×
545
  m_currentDir =
546
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
547
  // Clear any previously cached clipped text before showing new password
548
  m_qtPass->clearClippedText();
×
549
  QString file = getFile(index, true);
×
550
  ui->passwordName->setText(file);
×
551
  if (!file.isEmpty() && !cleared) {
×
552
    QtPassSettings::getPass()->Show(file);
×
553
  } else {
554
    clearPanel(false);
×
555
    ui->actionEdit->setEnabled(false);
×
556
    ui->actionDelete->setEnabled(true);
×
557
  }
558
}
×
559

560
/**
561
 * @brief MainWindow::on_treeView_doubleClicked when doubleclicked on
562
 * TreeViewItem, open the edit Window
563
 * @param index
564
 */
565
void MainWindow::on_treeView_doubleClicked(const QModelIndex &index) {
×
566
  QFileInfo fileOrFolder =
567
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
568

569
  if (fileOrFolder.isFile()) {
×
570
    editPassword(getFile(index, true));
×
571
  }
572
}
×
573

574
/**
575
 * @brief MainWindow::deselect clear the selection, password and copy buffer
576
 */
577
void MainWindow::deselect() {
×
578
  m_currentDir = "";
×
579
  m_qtPass->clearClipboard();
×
580
  ui->treeView->clearSelection();
×
581
  ui->actionEdit->setEnabled(false);
×
582
  ui->actionDelete->setEnabled(false);
×
583
  ui->passwordName->setText("");
×
584
  clearPanel(false);
×
585
}
×
586

587
void MainWindow::executeWrapperStarted() {
×
588
  clearTemplateWidgets();
×
589
  ui->textBrowser->clear();
×
590
  setUiElementsEnabled(false);
×
591
  clearPanelTimer.stop();
×
592
  if (QtPassSettings::isShowProcessOutput()) {
×
593
    m_processOutputDock->setVisible(true);
×
594
  }
595
}
×
596

597
/**
598
 * @brief Handles displaying parsed password entry content in the main window.
599
 * @example
600
 * void result = MainWindow::passShowHandler(p_output);
601
 * // Updates the UI with parsed fields and emits
602
 * passShowHandlerFinished(output)
603
 *
604
 * @param p_output - The raw output text containing the password entry data.
605
 * @return void - This function does not return a value.
606
 */
607
void MainWindow::passShowHandler(const QString &p_output) {
×
608
  QStringList templ = QtPassSettings::isUseTemplate()
×
609
                          ? QtPassSettings::getPassTemplate().split("\n")
×
610
                          : QStringList();
×
611
  bool allFields =
612
      QtPassSettings::isUseTemplate() && QtPassSettings::isTemplateAllFields();
×
613
  FileContent fileContent = FileContent::parse(p_output, templ, allFields);
×
614
  QString output = p_output;
615
  QString password = fileContent.getPassword();
×
616

617
  // set clipped text
618
  m_qtPass->setClippedText(password, p_output);
×
619

620
  // first clear the current view:
621
  clearTemplateWidgets();
×
622

623
  // show what is needed:
624
  if (QtPassSettings::isHideContent()) {
×
625
    output = "***" + tr("Content hidden") + "***";
×
626
  } else if (!QtPassSettings::isDisplayAsIs()) {
×
627
    if (!password.isEmpty()) {
×
628
      // set the password, it is hidden if needed in addToGridLayout
629
      addToGridLayout(0, tr("Password"), password);
×
630
    }
631

632
    NamedValues namedValues = fileContent.getNamedValues();
×
633
    for (int j = 0; j < namedValues.length(); ++j) {
×
634
      const NamedValue &nv = namedValues.at(j);
635
      addToGridLayout(j + 1, nv.name, nv.value);
×
636
    }
637
    if (ui->gridLayout->count() == 0) {
×
638
      ui->verticalLayoutPassword->setSpacing(0);
×
639
    } else {
640
      ui->verticalLayoutPassword->setSpacing(6);
×
641
    }
642

643
    output = fileContent.getRemainingDataForDisplay();
×
644
  }
645

646
  if (QtPassSettings::isUseAutoclearPanel()) {
×
647
    clearPanelTimer.start();
×
648
  }
649

650
  emit passShowHandlerFinished(output);
×
651
  setUiElementsEnabled(true);
×
652
}
×
653

654
/**
655
 * @brief Handles the OTP output by displaying it, copying it to the clipboard,
656
 * and updating the UI state.
657
 * @example
658
 * void MainWindow::passOtpHandler(const QString &p_output);
659
 *
660
 * @param const QString &p_output - The OTP code text to process; if empty, an
661
 * error message is shown instead.
662
 * @return void - This function does not return a value.
663
 */
664
void MainWindow::passOtpHandler(const QString &p_output) {
×
665
  if (!p_output.isEmpty()) {
×
666
    addToGridLayout(ui->gridLayout->count() + 1, tr("OTP Code"), p_output);
×
667
    m_qtPass->copyTextToClipboard(p_output);
×
668
    showStatusMessage(tr("OTP code copied to clipboard"));
×
669
  } else {
670
    flashText(tr("No OTP code found in this password entry"), true);
×
671
  }
672
  if (QtPassSettings::isUseAutoclearPanel()) {
×
673
    clearPanelTimer.start();
×
674
  }
675
  setUiElementsEnabled(true);
×
676
}
×
677

678
/**
679
 * @brief MainWindow::clearPanel hide the information from shoulder surfers
680
 */
681
void MainWindow::clearPanel(bool notify) {
×
682
  while (ui->gridLayout->count() > 0) {
×
683
    QLayoutItem *item = ui->gridLayout->takeAt(0);
×
684
    delete item->widget();
×
685
    delete item;
×
686
  }
687
  const bool grepWasVisible = ui->grepResultsList->isVisible();
×
688
  ui->grepResultsList->clear();
×
689
  if (grepWasVisible) {
×
690
    ui->grepResultsList->setVisible(false);
×
691
    ui->treeView->setVisible(true);
×
692
    if (m_grepMode) {
×
693
      m_grepMode = false;
×
694
      ui->grepButton->blockSignals(true);
×
695
      ui->grepButton->setChecked(false);
×
696
      ui->grepButton->blockSignals(false);
×
697
      ui->lineEdit->blockSignals(true);
×
698
      ui->lineEdit->clear();
×
699
      ui->lineEdit->blockSignals(false);
×
700
      ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
701
    }
702
  }
703
  if (notify) {
×
704
    QString output = "***" + tr("Password and Content hidden") + "***";
×
705
    ui->textBrowser->setHtml(output);
×
706
  } else {
707
    ui->textBrowser->setHtml("");
×
708
  }
709
}
×
710

711
/**
712
 * @brief MainWindow::setUiElementsEnabled enable or disable the relevant UI
713
 * elements
714
 * @param state
715
 */
716
void MainWindow::setUiElementsEnabled(bool state) {
×
717
  ui->treeView->setEnabled(state);
×
718
  ui->lineEdit->setEnabled(state);
×
719
  ui->lineEdit->installEventFilter(this);
×
720
  ui->actionAddPassword->setEnabled(state);
×
721
  ui->actionAddFolder->setEnabled(state);
×
722
  ui->actionUsers->setEnabled(state);
×
723
  ui->actionConfig->setEnabled(state);
×
724
  // is a file selected?
725
  state &= ui->treeView->currentIndex().isValid();
×
726
  ui->actionDelete->setEnabled(state);
×
727
  ui->actionEdit->setEnabled(state);
×
728
  updateGitButtonVisibility();
×
729
  updateOtpButtonVisibility();
×
730
}
×
731

732
/**
733
 * @brief Restores the main window geometry, state, position, size, and
734
 * tray/icon settings from saved application settings.
735
 * @example
736
 * MainWindow window;
737
 * window.restoreWindow();
738
 *
739
 * @return void - This function does not return a value.
740
 */
741
void MainWindow::restoreWindow() {
×
742
  QByteArray geometry = QtPassSettings::getGeometry(saveGeometry());
×
743
  restoreGeometry(geometry);
×
744
  QByteArray savestate = QtPassSettings::getSavestate(saveState());
×
745
  restoreState(savestate);
×
746
  QPoint position = QtPassSettings::getPos(pos());
×
747
  move(position);
×
748
  QSize newSize = QtPassSettings::getSize(size());
×
749
  resize(newSize);
×
750
  if (QtPassSettings::isMaximized(isMaximized())) {
×
751
    showMaximized();
×
752
  }
753

754
  if (QtPassSettings::isAlwaysOnTop()) {
×
755
    Qt::WindowFlags flags = windowFlags();
756
    setWindowFlags(flags | Qt::WindowStaysOnTopHint);
×
757
    show();
×
758
  }
759

760
  if (QtPassSettings::isUseTrayIcon() && m_tray == nullptr) {
×
761
    initTrayIcon();
×
762
    if (QtPassSettings::isStartMinimized()) {
×
763
      // since we are still in constructor, can't directly hide
764
      QTimer::singleShot(10, this, SLOT(hide()));
×
765
    }
766
  } else if (!QtPassSettings::isUseTrayIcon() && m_tray != nullptr) {
×
767
    destroyTrayIcon();
×
768
  }
769
}
×
770

771
/**
772
 * @brief MainWindow::on_configButton_clicked run Mainwindow::config
773
 */
774
void MainWindow::onConfig() { config(); }
×
775

776
/**
777
 * @brief Executes when the string in the search box changes, collapses the
778
 * TreeView
779
 * @param arg1
780
 */
781
void MainWindow::on_lineEdit_textChanged(const QString &arg1) {
×
782
  if (m_grepMode)
×
783
    return;
784
  ui->statusBar->showMessage(tr("Looking for: %1").arg(arg1), 1000);
×
785
  ui->treeView->expandAll();
×
786
  clearPanel(false);
×
787
  ui->passwordName->setText("");
×
788
  ui->actionEdit->setEnabled(false);
×
789
  ui->actionDelete->setEnabled(false);
×
790
  searchTimer.start();
×
791
}
792

793
/**
794
 * @brief MainWindow::onTimeoutSearch Fired when search is finished or too much
795
 * time from two keypresses is elapsed
796
 */
797
void MainWindow::onTimeoutSearch() {
×
798
  QString query = ui->lineEdit->text();
×
799

800
  if (query.isEmpty()) {
×
801
    ui->treeView->collapseAll();
×
802
    deselect();
×
803
  }
804

805
  query.replace(QStringLiteral(" "), ".*");
×
806
  QRegularExpression regExp(query, QRegularExpression::CaseInsensitiveOption);
×
807
  proxyModel.setFilterRegularExpression(regExp);
×
808
  ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
809
      model.setRootPath(QtPassSettings::getPassStore())));
×
810

811
  if (proxyModel.rowCount() > 0 && !query.isEmpty()) {
×
812
    selectFirstFile();
×
813
  } else {
814
    ui->actionEdit->setEnabled(false);
×
815
    ui->actionDelete->setEnabled(false);
×
816
  }
817
}
×
818

819
/**
820
 * @brief MainWindow::on_lineEdit_returnPressed get searching
821
 *
822
 * Select the first possible file in the tree
823
 */
824
void MainWindow::on_lineEdit_returnPressed() {
×
825
#ifdef QT_DEBUG
826
  dbg() << "on_lineEdit_returnPressed" << proxyModel.rowCount();
827
#endif
828

829
  if (m_grepMode) {
×
830
    const QString query = ui->lineEdit->text();
×
831
    if (!query.isEmpty()) {
×
832
      m_grepCancelled = false;
×
833
      ui->grepResultsList->clear();
×
834
      ui->statusBar->showMessage(tr("Searching…"));
×
835
      if (!m_grepBusy) {
×
836
        m_grepBusy = true;
×
837
        QApplication::setOverrideCursor(Qt::WaitCursor);
×
838
      }
839
      QtPassSettings::getPass()->Grep(query, ui->grepCaseButton->isChecked());
×
840
    } else {
841
      m_grepCancelled = true;
×
842
      if (m_grepBusy) {
×
843
        m_grepBusy = false;
×
844
        QApplication::restoreOverrideCursor();
×
845
      }
846
      ui->grepResultsList->clear();
×
847
      ui->grepResultsList->setVisible(false);
×
848
      ui->treeView->setVisible(true);
×
849
    }
850
    return;
851
  }
852

853
  if (proxyModel.rowCount() > 0) {
×
854
    selectFirstFile();
×
855
    on_treeView_clicked(ui->treeView->currentIndex());
×
856
  }
857
}
858

859
/**
860
 * @brief Toggle grep (content search) mode.
861
 */
862
void MainWindow::on_grepButton_toggled(bool checked) {
×
863
  m_grepMode = checked;
×
864
  if (checked) {
×
865
    ui->lineEdit->setPlaceholderText(tr("Search content (regex)"));
×
866
    ui->lineEdit->clear();
×
867
    searchTimer.stop();
×
868
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
869
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
870
        model.setRootPath(QtPassSettings::getPassStore())));
×
871
    ui->grepResultsList->setVisible(false);
×
872
    // Keep treeView visible until results arrive
873
  } else {
874
    if (m_grepBusy) {
×
875
      m_grepBusy = false;
×
876
      m_grepCancelled = true;
×
877
      QApplication::restoreOverrideCursor();
×
878
    }
879
    searchTimer.stop();
×
880
    ui->lineEdit->blockSignals(true);
×
881
    ui->lineEdit->clear();
×
882
    ui->lineEdit->blockSignals(false);
×
883
    ui->lineEdit->setPlaceholderText(tr("Search Password"));
×
884
    ui->grepResultsList->clear();
×
885
    ui->grepResultsList->setVisible(false);
×
886
    ui->treeView->setVisible(true);
×
887
    proxyModel.setFilterRegularExpression(QRegularExpression());
×
888
    ui->treeView->setRootIndex(proxyModel.mapFromSource(
×
889
        model.setRootPath(QtPassSettings::getPassStore())));
×
890
  }
891
}
×
892

893
/**
894
 * @brief Display grep results in grepResultsList.
895
 */
896
void MainWindow::onGrepFinished(
×
897
    const QList<QPair<QString, QStringList>> &results) {
898
  if (m_grepBusy) {
×
899
    m_grepBusy = false;
×
900
    QApplication::restoreOverrideCursor();
×
901
  }
902
  if (m_grepCancelled) {
×
903
    m_grepCancelled = false;
×
904
    return;
×
905
  }
906
  setUiElementsEnabled(true);
×
907
  if (!m_grepMode)
×
908
    return;
909
  ui->grepResultsList->clear();
×
910
  if (results.isEmpty()) {
×
911
    ui->statusBar->showMessage(tr("No matches found."), 3000);
×
912
    ui->grepResultsList->setVisible(false);
×
913
    ui->treeView->setVisible(true);
×
914
    return;
×
915
  }
916
  const bool hideContent = QtPassSettings::isHideContent();
×
917
  int totalLines = 0;
918
  for (const auto &pair : results) {
×
919
    auto *entryItem = new QTreeWidgetItem(ui->grepResultsList);
×
920
    entryItem->setText(0, pair.first);
×
921
    entryItem->setData(0, Qt::UserRole, pair.first);
×
922
    for (const QString &line : pair.second) {
×
923
      auto *lineItem = new QTreeWidgetItem(entryItem);
×
924
      lineItem->setText(0, hideContent ? "***" + tr("Content hidden") + "***"
×
925
                                       : line);
926
      lineItem->setData(0, Qt::UserRole, pair.first);
×
927
      ++totalLines;
×
928
    }
929
  }
930
  ui->grepResultsList->expandAll();
×
931
  ui->treeView->setVisible(false);
×
932
  ui->grepResultsList->setVisible(true);
×
933
  ui->statusBar->showMessage(
×
934
      tr("Found %n match(es)", nullptr, totalLines) + " " +
×
935
          tr("in %n entr(ies).", nullptr, static_cast<int>(results.size())),
×
936
      3000);
937
  if (QtPassSettings::isUseAutoclearPanel())
×
938
    clearPanelTimer.start();
×
939
}
940

941
/**
942
 * @brief Navigate to the password entry when a grep result is clicked.
943
 */
944
void MainWindow::on_grepResultsList_itemClicked(QTreeWidgetItem *item,
×
945
                                                int /*column*/) {
946
  const QString entry = item->data(0, Qt::UserRole).toString();
×
947
  if (entry.isEmpty())
×
948
    return;
949
  const QString fullPath = QDir::cleanPath(
950
      QDir(QtPassSettings::getPassStore()).filePath(entry + ".gpg"));
×
951
  QModelIndex srcIndex = model.index(fullPath);
×
952
  if (!srcIndex.isValid())
953
    return;
954
  QModelIndex proxyIndex = proxyModel.mapFromSource(srcIndex);
×
955
  if (!proxyIndex.isValid())
956
    return;
957
  ui->treeView->setCurrentIndex(proxyIndex);
×
958
  on_treeView_clicked(proxyIndex);
×
959
  if (QtPassSettings::isHideContent() || QtPassSettings::isUseAutoclearPanel())
×
960
    ui->grepResultsList->clear();
×
961
  ui->grepResultsList->setVisible(false);
×
962
  ui->treeView->setVisible(true);
×
963
  ui->treeView->scrollTo(proxyIndex);
×
964
  ui->treeView->setFocus();
×
965
}
966

967
/**
968
 * @brief MainWindow::selectFirstFile select the first possible file in the
969
 * tree
970
 */
971
void MainWindow::selectFirstFile() {
×
972
  QModelIndex index = proxyModel.mapFromSource(
×
973
      model.setRootPath(QtPassSettings::getPassStore()));
×
974
  index = firstFile(index);
×
975
  ui->treeView->setCurrentIndex(index);
×
976
}
×
977

978
/**
979
 * @brief MainWindow::firstFile return location of first possible file
980
 * @param parentIndex
981
 * @return QModelIndex
982
 */
983
auto MainWindow::firstFile(QModelIndex parentIndex) -> QModelIndex {
×
984
  QModelIndex index = parentIndex;
×
985
  int numRows = proxyModel.rowCount(parentIndex);
×
986
  for (int row = 0; row < numRows; ++row) {
×
987
    index = proxyModel.index(row, 0, parentIndex);
×
988
    if (model.fileInfo(proxyModel.mapToSource(index)).isFile()) {
×
989
      return index;
×
990
    }
991
    if (proxyModel.hasChildren(index)) {
×
992
      return firstFile(index);
×
993
    }
994
  }
995
  return index;
×
996
}
997

998
/**
999
 * @brief MainWindow::confirmPathInStore reject paths that resolve outside
1000
 * the password store and warn the user.
1001
 *
1002
 * Used before file/folder creation, move, and rename to stop user-typed
1003
 * names like "../../etc/passwd" or absolute paths from escaping the
1004
 * configured store root via the input dialogs.
1005
 *
1006
 * @param candidate Absolute candidate path to validate.
1007
 * @return true if the path is inside the password store; false otherwise (a
1008
 * warning dialog is shown in that case).
1009
 */
NEW
1010
auto MainWindow::confirmPathInStore(const QString &candidate) -> bool {
×
NEW
1011
  if (Util::isPathInStore(QtPassSettings::getPassStore(), candidate)) {
×
1012
    return true;
1013
  }
NEW
1014
  QMessageBox::warning(this, tr("Invalid name"),
×
NEW
1015
                       tr("That name would resolve outside the password "
×
1016
                          "store. Please choose a different name."));
NEW
1017
  return false;
×
1018
}
1019

1020
/**
1021
 * @brief MainWindow::setPassword open passworddialog
1022
 * @param file which pgp file
1023
 * @param isNew insert (not update)
1024
 */
1025
void MainWindow::setPassword(const QString &file, bool isNew) {
×
1026
  PasswordDialog d(file, isNew, this);
×
1027

1028
  if (isNew) {
×
1029
    QString storePath = QtPassSettings::getPassStore();
×
1030
    QString folder =
1031
        Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1032
    if (folder.isEmpty()) {
×
1033
      folder = storePath;
×
1034
    }
1035
    QHash<QString, QStringList> templates = Util::readTemplates(storePath);
×
1036
    if (!templates.isEmpty()) {
1037
      QString defaultTemplate = Util::getFolderTemplate(folder, storePath);
×
1038
      d.setAvailableTemplates(templates, defaultTemplate);
×
1039
      new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_T), &d,
×
1040
                    [&d]() { d.cycleTemplate(); });
×
1041
    }
1042
  }
×
1043

1044
  if (!d.exec()) {
×
1045
    ui->treeView->setFocus();
×
1046
  }
1047
}
×
1048

1049
/**
1050
 * @brief MainWindow::addPassword add a new password by showing a
1051
 * number of dialogs.
1052
 */
1053
void MainWindow::addPassword() {
×
1054
  bool ok;
1055
  QString dir =
1056
      Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
1057
  QString file =
1058
      QInputDialog::getText(this, tr("New file"),
×
1059
                            tr("New password file: \n(Will be placed in %1 )")
×
1060
                                .arg(QtPassSettings::getPassStore() +
×
1061
                                     Util::getDir(ui->treeView->currentIndex(),
×
1062
                                                  true, model, proxyModel)),
1063
                            QLineEdit::Normal, "", &ok);
×
1064
  if (!ok || file.isEmpty()) {
×
1065
    return;
1066
  }
1067
  file = dir + file;
×
NEW
1068
  if (!confirmPathInStore(QtPassSettings::getPassStore() + file)) {
×
1069
    return;
1070
  }
UNCOV
1071
  setPassword(file);
×
1072
}
1073

1074
/**
1075
 * @brief MainWindow::onDelete remove password, if you are
1076
 * sure.
1077
 */
1078
void MainWindow::onDelete() {
×
1079
  QModelIndex currentIndex = ui->treeView->currentIndex();
×
1080
  if (!currentIndex.isValid()) {
1081
    // This fixes https://github.com/IJHack/QtPass/issues/556
1082
    // Otherwise the entire password directory would be deleted if
1083
    // nothing is selected in the tree view.
1084
    return;
×
1085
  }
1086

1087
  QFileInfo fileOrFolder =
1088
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1089
  QString file = "";
×
1090
  bool isDir = false;
1091

1092
  if (fileOrFolder.isFile()) {
×
1093
    file = getFile(ui->treeView->currentIndex(), true);
×
1094
  } else {
1095
    file = Util::getDir(ui->treeView->currentIndex(), true, model, proxyModel);
×
1096
    isDir = true;
1097
  }
1098

1099
  QString dirMessage = tr(" and the whole content?");
1100
  if (isDir) {
×
1101
    QDirIterator it(model.rootPath() + QDir::separator() + file,
×
1102
                    QDirIterator::Subdirectories);
×
1103
    bool okDir = true;
1104
    while (it.hasNext() && okDir) {
×
1105
      it.next();
×
1106
      if (QFileInfo(it.filePath()).isFile()) {
×
1107
        if (QFileInfo(it.filePath()).suffix() != "gpg") {
×
1108
          okDir = false;
1109
          dirMessage = tr(" and the whole content? <br><strong>Attention: "
×
1110
                          "there are unexpected files in the given folder, "
1111
                          "check them before continue.</strong>");
1112
        }
1113
      }
1114
    }
1115
  }
×
1116

1117
  if (QMessageBox::question(
×
1118
          this, isDir ? tr("Delete folder?") : tr("Delete password?"),
×
1119
          tr("Are you sure you want to delete %1%2?")
×
1120
              .arg(QDir::separator() + file, isDir ? dirMessage : "?"),
×
1121
          QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
1122
    return;
1123
  }
1124

1125
  QtPassSettings::getPass()->Remove(file, isDir);
×
1126
}
×
1127

1128
/**
1129
 * @brief MainWindow::onOTP try and generate (selected) OTP code.
1130
 */
1131
void MainWindow::onOtp() {
×
1132
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1133
  if (!file.isEmpty()) {
×
1134
    if (QtPassSettings::isUseOtp()) {
×
1135
      setUiElementsEnabled(false);
×
1136
      QtPassSettings::getPass()->OtpGenerate(file);
×
1137
    }
1138
  } else {
1139
    flashText(tr("No password selected for OTP generation"), true);
×
1140
  }
1141
}
×
1142

1143
/**
1144
 * @brief MainWindow::onEdit try and edit (selected) password.
1145
 */
1146
void MainWindow::onEdit() {
×
1147
  QString file = getFile(ui->treeView->currentIndex(), true);
×
1148
  editPassword(file);
×
1149
}
×
1150

1151
/**
1152
 * @brief MainWindow::userDialog see MainWindow::onUsers()
1153
 * @param dir folder to edit users for.
1154
 */
1155
void MainWindow::userDialog(const QString &dir) {
×
1156
  if (!dir.isEmpty()) {
×
1157
    m_currentDir = dir;
×
1158
  }
1159
  onUsers();
×
1160
}
×
1161

1162
/**
1163
 * @brief MainWindow::onUsers edit users for the current
1164
 * folder,
1165
 * gets lists and opens UserDialog.
1166
 */
1167
void MainWindow::onUsers() {
×
1168
  QString dir =
1169
      m_currentDir.isEmpty()
1170
          ? Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel)
×
1171
          : m_currentDir;
×
1172

1173
  UsersDialog d(dir, this);
×
1174
  if (!d.exec()) {
×
1175
    ui->treeView->setFocus();
×
1176
  }
1177
}
×
1178

1179
/**
1180
 * @brief MainWindow::messageAvailable we have some text/message/search to do.
1181
 * @param message
1182
 */
1183
void MainWindow::messageAvailable(const QString &message) {
×
1184
  show();
×
1185
  raise();
×
1186
  if (message.isEmpty()) {
×
1187
    focusInput();
×
1188
  } else {
1189
    ui->treeView->expandAll();
×
1190
    ui->lineEdit->setText(message);
×
1191
    on_lineEdit_returnPressed();
×
1192
  }
1193
}
×
1194

1195
/**
1196
 * @brief MainWindow::generateKeyPair internal gpg keypair generator . .
1197
 * @param batch
1198
 * @param keygenWindow
1199
 */
1200
void MainWindow::generateKeyPair(const QString &batch, QDialog *keygenWindow) {
×
1201
  m_keygenDialog = keygenWindow;
×
1202
  emit generateGPGKeyPair(batch);
×
1203
}
×
1204

1205
/**
1206
 * @brief MainWindow::updateProfileBox update the list of profiles, optionally
1207
 * select a more appropriate one to view too
1208
 */
1209
void MainWindow::updateProfileBox() {
×
1210
  QHash<QString, QHash<QString, QString>> profiles =
1211
      QtPassSettings::getProfiles();
×
1212

1213
  if (profiles.isEmpty()) {
1214
    ui->profileWidget->hide();
×
1215
  } else {
1216
    ui->profileWidget->show();
×
1217
    ui->profileBox->setEnabled(profiles.size() > 1);
×
1218
    ui->profileBox->clear();
×
1219
    QHashIterator<QString, QHash<QString, QString>> i(profiles);
×
1220
    while (i.hasNext()) {
×
1221
      i.next();
1222
      if (!i.key().isEmpty()) {
×
1223
        ui->profileBox->addItem(i.key());
×
1224
      }
1225
    }
1226
    ui->profileBox->model()->sort(0);
×
1227
  }
1228
  int index = ui->profileBox->findText(QtPassSettings::getProfile());
×
1229
  if (index != -1) { //  -1 for not found
×
1230
    ui->profileBox->setCurrentIndex(index);
×
1231
  }
1232
}
×
1233

1234
/**
1235
 * @brief MainWindow::on_profileBox_currentIndexChanged make sure we show the
1236
 * correct "profile"
1237
 * @param name
1238
 */
1239
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1240
void MainWindow::on_profileBox_currentIndexChanged(QString name) {
1241
#else
1242
/**
1243
 * @brief Handles changes to the selected profile in the profile combo box.
1244
 * @details Ignores the event during a fresh start or when the selected profile
1245
 * matches the current profile. Otherwise, it clears the password field, updates
1246
 * the active profile and related settings, refreshes the environment, and
1247
 * resets the tree view and action states to reflect the newly selected profile.
1248
 *
1249
 * @param name - The newly selected profile name.
1250
 * @return void - This function does not return a value.
1251
 *
1252
 */
1253
void MainWindow::on_profileBox_currentTextChanged(const QString &name) {
×
1254
#endif
1255
  if (m_qtPass->isFreshStart() || name == QtPassSettings::getProfile()) {
×
1256
    return;
×
1257
  }
1258

1259
  ui->lineEdit->clear();
×
1260

1261
  QtPassSettings::setProfile(name);
×
1262

1263
  QtPassSettings::setPassStore(
×
1264
      QtPassSettings::getProfiles().value(name).value("path"));
×
1265
  QtPassSettings::setPassSigningKey(
×
1266
      QtPassSettings::getProfiles().value(name).value("signingKey"));
×
1267
  ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000);
×
1268

1269
  QtPassSettings::getPass()->updateEnv();
×
1270

1271
  const QString passStore = QtPassSettings::getPassStore();
×
1272
  proxyModel.setStore(passStore);
×
1273
  ui->treeView->setRootIndex(
×
1274
      proxyModel.mapFromSource(model.setRootPath(passStore)));
×
1275
  deselect();
×
1276
  ui->treeView->setCurrentIndex(QModelIndex());
×
1277
}
1278

1279
/**
1280
 * @brief MainWindow::initTrayIcon show a nice tray icon on systems that
1281
 * support
1282
 * it
1283
 */
1284
void MainWindow::initTrayIcon() {
×
1285
  m_tray = new TrayIcon(this);
×
1286
  // Setup tray icon
1287

1288
  if (m_tray == nullptr) {
1289
#ifdef QT_DEBUG
1290
    dbg() << "Allocating tray icon failed.";
1291
#endif
1292
    return;
1293
  }
1294

1295
  if (!m_tray->getIsAllocated()) {
×
1296
    destroyTrayIcon();
×
1297
  }
1298
}
1299

1300
/**
1301
 * @brief MainWindow::destroyTrayIcon remove that pesky tray icon
1302
 */
1303
void MainWindow::destroyTrayIcon() {
×
1304
  delete m_tray;
×
1305
  m_tray = nullptr;
×
1306
}
×
1307

1308
/**
1309
 * @brief MainWindow::closeEvent hide or quit
1310
 * @param event
1311
 */
1312
void MainWindow::closeEvent(QCloseEvent *event) {
×
1313
  if (QtPassSettings::isHideOnClose()) {
×
1314
    this->hide();
×
1315
    event->ignore();
1316
  } else {
1317
    m_qtPass->clearClipboard();
×
1318

1319
    QtPassSettings::setGeometry(saveGeometry());
×
1320
    QtPassSettings::setSavestate(saveState());
×
1321
    QtPassSettings::setMaximized(isMaximized());
×
1322
    if (!isMaximized()) {
×
1323
      QtPassSettings::setPos(pos());
×
1324
      QtPassSettings::setSize(size());
×
1325
    }
1326
    event->accept();
1327
  }
1328
}
×
1329

1330
/**
1331
 * @brief MainWindow::eventFilter filter out some events and focus the
1332
 * treeview
1333
 * @param obj
1334
 * @param event
1335
 * @return
1336
 */
1337
auto MainWindow::eventFilter(QObject *obj, QEvent *event) -> bool {
×
1338
  if (obj == ui->lineEdit && event->type() == QEvent::KeyPress) {
×
1339
    auto *key = dynamic_cast<QKeyEvent *>(event);
×
1340
    if (key != nullptr && key->key() == Qt::Key_Down) {
×
1341
      ui->treeView->setFocus();
×
1342
    }
1343
  }
1344
  return QObject::eventFilter(obj, event);
×
1345
}
1346

1347
/**
1348
 * @brief MainWindow::keyPressEvent did anyone press return, enter or escape?
1349
 * @param event
1350
 */
1351
void MainWindow::keyPressEvent(QKeyEvent *event) {
×
1352
  switch (event->key()) {
×
1353
  case Qt::Key_Delete:
×
1354
    onDelete();
×
1355
    break;
×
1356
  case Qt::Key_Return:
×
1357
  case Qt::Key_Enter:
1358
    if (proxyModel.rowCount() > 0) {
×
1359
      on_treeView_clicked(ui->treeView->currentIndex());
×
1360
    }
1361
    break;
1362
  case Qt::Key_Escape:
×
1363
    ui->lineEdit->clear();
×
1364
    break;
×
1365
  default:
1366
    break;
1367
  }
1368
}
×
1369

1370
/**
1371
 * @brief MainWindow::showContextMenu show us the (file or folder) context
1372
 * menu
1373
 * @param pos
1374
 */
1375
void MainWindow::showContextMenu(const QPoint &pos) {
×
1376
  QModelIndex index = ui->treeView->indexAt(pos);
×
1377
  bool selected = true;
1378
  if (!index.isValid()) {
1379
    ui->treeView->clearSelection();
×
1380
    ui->actionDelete->setEnabled(false);
×
1381
    ui->actionEdit->setEnabled(false);
×
1382
    m_currentDir = "";
×
1383
    selected = false;
1384
  }
1385

1386
  ui->treeView->setCurrentIndex(index);
×
1387

1388
  QPoint globalPos = ui->treeView->viewport()->mapToGlobal(pos);
×
1389

1390
  QFileInfo fileOrFolder =
1391
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1392

1393
  QMenu contextMenu;
×
1394
  if (!selected || fileOrFolder.isDir()) {
×
1395
    QAction *openFolder =
1396
        contextMenu.addAction(tr("Open folder with file manager"));
×
1397
    QAction *addFolder = contextMenu.addAction(tr("Add folder"));
×
1398
    QAction *addPassword = contextMenu.addAction(tr("Add password"));
×
1399
    QAction *users = contextMenu.addAction(tr("Users"));
×
1400
    connect(openFolder, &QAction::triggered, this, &MainWindow::openFolder);
×
1401
    connect(addFolder, &QAction::triggered, this, &MainWindow::addFolder);
×
1402
    connect(addPassword, &QAction::triggered, this, &MainWindow::addPassword);
×
1403
    connect(users, &QAction::triggered, this, &MainWindow::onUsers);
×
1404
  } else if (fileOrFolder.isFile()) {
×
1405
    QAction *edit = contextMenu.addAction(tr("Edit"));
×
1406
    connect(edit, &QAction::triggered, this, &MainWindow::onEdit);
×
1407
  }
1408
  if (selected) {
×
1409
    contextMenu.addSeparator();
×
1410
    if (fileOrFolder.isDir()) {
×
1411
      QAction *renameFolder = contextMenu.addAction(tr("Rename folder"));
×
1412
      connect(renameFolder, &QAction::triggered, this,
×
1413
              &MainWindow::renameFolder);
×
1414
    } else if (fileOrFolder.isFile()) {
×
1415
      QAction *renamePassword = contextMenu.addAction(tr("Rename password"));
×
1416
      connect(renamePassword, &QAction::triggered, this,
×
1417
              &MainWindow::renamePassword);
×
1418
    }
1419
    QAction *deleteItem = contextMenu.addAction(tr("Delete"));
×
1420
    connect(deleteItem, &QAction::triggered, this, &MainWindow::onDelete);
×
1421
    if (fileOrFolder.isDir()) {
×
1422
      QString dirPath = QDir::cleanPath(
1423
          Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1424

1425
      auto *shareMenu = new QMenu(tr("Share"), &contextMenu);
×
1426
      contextMenu.addMenu(shareMenu);
×
1427

1428
      QString gpgIdPath = Pass::getGpgIdPath(dirPath);
×
1429
      bool gpgIdExists = !gpgIdPath.isEmpty() && QFile(gpgIdPath).exists();
×
1430

1431
      QString exePath = QtPassSettings::isUsePass()
×
1432
                            ? QtPassSettings::getPassExecutable()
×
1433
                            : QtPassSettings::getGpgExecutable();
×
1434
      bool gpgAvailable = !exePath.isEmpty() && (exePath.startsWith("wsl ") ||
×
1435
                                                 QFile(exePath).exists());
×
1436

1437
      QAction *reencrypt = shareMenu->addAction(tr("Re-encrypt all passwords"));
×
1438
      reencrypt->setEnabled(gpgIdExists && gpgAvailable);
×
1439
      connect(reencrypt, &QAction::triggered, this,
×
1440
              [this, dirPath]() { reencryptPath(dirPath); });
×
1441

1442
      QAction *exportKey = shareMenu->addAction(tr("Export my public key..."));
×
1443
      exportKey->setEnabled(gpgAvailable);
×
1444
      connect(exportKey, &QAction::triggered, this,
×
1445
              &MainWindow::exportPublicKey);
×
1446

1447
      QAction *addRecipientAction =
1448
          shareMenu->addAction(tr("Add recipient..."));
×
1449
      addRecipientAction->setEnabled(gpgIdExists && gpgAvailable);
×
1450
      connect(addRecipientAction, &QAction::triggered, this,
×
1451
              [this, dirPath]() { addRecipient(dirPath); });
×
1452

1453
      QAction *shareHelp = shareMenu->addAction(tr("What is this?"));
×
1454
      connect(shareHelp, &QAction::triggered, this, &MainWindow::showShareHelp);
×
1455
    }
1456
  }
1457
  contextMenu.exec(globalPos);
×
1458
}
×
1459

1460
/**
1461
 * @brief MainWindow::showBrowserContextMenu show us the context menu in
1462
 * password window
1463
 * @param pos
1464
 */
1465
void MainWindow::showBrowserContextMenu(const QPoint &pos) {
×
1466
  QMenu *contextMenu = ui->textBrowser->createStandardContextMenu(pos);
×
1467
  QPoint globalPos = ui->textBrowser->viewport()->mapToGlobal(pos);
×
1468

1469
  contextMenu->exec(globalPos);
×
1470
  delete contextMenu;
×
1471
}
×
1472

1473
/**
1474
 * @brief MainWindow::openFolder open the folder in the default file manager
1475
 */
1476
void MainWindow::openFolder() {
×
1477
  QString dir =
1478
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1479

1480
  QString path = QDir::toNativeSeparators(dir);
×
1481
  QDesktopServices::openUrl(QUrl::fromLocalFile(path));
×
1482
}
×
1483

1484
/**
1485
 * @brief MainWindow::addFolder add a new folder to store passwords in
1486
 */
1487
void MainWindow::addFolder() {
×
1488
  bool ok;
1489
  QString dir =
1490
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel);
×
1491
  QString newdir =
1492
      QInputDialog::getText(this, tr("New file"),
×
1493
                            tr("New Folder: \n(Will be placed in %1 )")
×
1494
                                .arg(QtPassSettings::getPassStore() +
×
1495
                                     Util::getDir(ui->treeView->currentIndex(),
×
1496
                                                  true, model, proxyModel)),
1497
                            QLineEdit::Normal, "", &ok);
×
1498
  if (!ok || newdir.isEmpty()) {
×
1499
    return;
1500
  }
1501
  newdir.prepend(dir);
NEW
1502
  if (!confirmPathInStore(newdir)) {
×
1503
    return;
1504
  }
1505
  if (!QDir().mkdir(newdir)) {
×
1506
    QMessageBox::warning(this, tr("Error"),
×
1507
                         tr("Failed to create folder: %1").arg(newdir));
×
1508
    return;
×
1509
  }
1510
  if (QtPassSettings::isAddGPGId(true)) {
×
1511
    QString gpgIdFile = newdir + "/.gpg-id";
×
1512
    QFile gpgId(gpgIdFile);
×
1513
    if (!gpgId.open(QIODevice::WriteOnly)) {
×
1514
      QMessageBox::warning(
×
1515
          this, tr("Error"),
×
1516
          tr("Failed to create .gpg-id file in: %1").arg(newdir));
×
1517
      return;
1518
    }
1519
    QList<UserInfo> users = QtPassSettings::getPass()->listKeys("", true);
×
1520
    for (const UserInfo &user : users) {
×
1521
      if (user.enabled) {
×
1522
        gpgId.write((user.key_id + "\n").toUtf8());
×
1523
      }
1524
    }
1525
    gpgId.close();
×
1526
  }
×
1527
}
1528

1529
/**
1530
 * @brief MainWindow::renameFolder rename an existing folder
1531
 */
1532
void MainWindow::renameFolder() {
×
1533
  bool ok;
1534
  QString srcDir = QDir::cleanPath(
1535
      Util::getDir(ui->treeView->currentIndex(), false, model, proxyModel));
×
1536
  QString srcDirName = QDir(srcDir).dirName();
×
1537
  QString newName =
1538
      QInputDialog::getText(this, tr("Rename file"), tr("Rename Folder To: "),
×
1539
                            QLineEdit::Normal, srcDirName, &ok);
×
1540
  if (!ok || newName.isEmpty()) {
×
1541
    return;
1542
  }
1543
  QString destDir = srcDir;
1544
  destDir.replace(srcDir.lastIndexOf(srcDirName), srcDirName.length(), newName);
×
NEW
1545
  if (!confirmPathInStore(destDir)) {
×
1546
    return;
1547
  }
UNCOV
1548
  QtPassSettings::getPass()->Move(srcDir, destDir);
×
1549
}
1550

1551
/**
1552
 * @brief MainWindow::editPassword read password and open edit window via
1553
 * MainWindow::onEdit()
1554
 */
1555
void MainWindow::editPassword(const QString &file) {
×
1556
  if (!file.isEmpty()) {
×
1557
    if (QtPassSettings::isUseGit() && QtPassSettings::isAutoPull()) {
×
1558
      onUpdate(true);
×
1559
    }
1560
    setPassword(file, false);
×
1561
  }
1562
}
×
1563

1564
/**
1565
 * @brief MainWindow::renamePassword rename an existing password
1566
 */
1567
void MainWindow::renamePassword() {
×
1568
  bool ok;
1569
  QString file = getFile(ui->treeView->currentIndex(), false);
×
1570
  QString filePath = QFileInfo(file).path();
×
1571
  QString fileName = QFileInfo(file).fileName();
×
1572
  if (fileName.endsWith(".gpg", Qt::CaseInsensitive)) {
×
1573
    fileName.chop(4);
×
1574
  }
1575

1576
  QString newName =
1577
      QInputDialog::getText(this, tr("Rename file"), tr("Rename File To: "),
×
1578
                            QLineEdit::Normal, fileName, &ok);
×
1579
  if (!ok || newName.isEmpty()) {
×
1580
    return;
1581
  }
1582
  QString newFile = QDir(filePath).filePath(newName);
×
NEW
1583
  if (!confirmPathInStore(newFile)) {
×
1584
    return;
1585
  }
UNCOV
1586
  QtPassSettings::getPass()->Move(file, newFile);
×
1587
}
1588

1589
/**
1590
 * @brief MainWindow::clearTemplateWidgets empty the template widget fields in
1591
 * the UI
1592
 */
1593
void MainWindow::clearTemplateWidgets() {
×
1594
  while (ui->gridLayout->count() > 0) {
×
1595
    QLayoutItem *item = ui->gridLayout->takeAt(0);
×
1596
    delete item->widget();
×
1597
    delete item;
×
1598
  }
1599
  ui->verticalLayoutPassword->setSpacing(0);
×
1600
}
×
1601

1602
/**
1603
 * @brief Copies the password of the selected file from the tree view to the
1604
 * clipboard.
1605
 * @example
1606
 * MainWindow::copyPasswordFromTreeview();
1607
 *
1608
 * @return void - This function does not return a value.
1609
 */
1610
void MainWindow::copyPasswordFromTreeview() {
×
1611
  QFileInfo fileOrFolder =
1612
      model.fileInfo(proxyModel.mapToSource(ui->treeView->currentIndex()));
×
1613

1614
  if (fileOrFolder.isFile()) {
×
1615
    QString file = getFile(ui->treeView->currentIndex(), true);
×
1616
    // Disconnect any previous connection to avoid accumulation
1617
    disconnect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1618
               &MainWindow::passwordFromFileToClipboard);
1619
    connect(QtPassSettings::getPass(), &Pass::finishedShow, this,
×
1620
            &MainWindow::passwordFromFileToClipboard);
×
1621
    QtPassSettings::getPass()->Show(file);
×
1622
  }
1623
}
×
1624

1625
void MainWindow::passwordFromFileToClipboard(const QString &text) {
×
1626
  QStringList tokens = text.split('\n');
×
1627
  m_qtPass->copyTextToClipboard(tokens[0]);
×
1628
}
×
1629

1630
/**
1631
 * @brief MainWindow::addToGridLayout add a field to the template grid
1632
 * @param position
1633
 * @param field
1634
 * @param value
1635
 */
1636
void MainWindow::addToGridLayout(int position, const QString &field,
×
1637
                                 const QString &value) {
1638
  QString trimmedField = field.trimmed();
1639
  QString trimmedValue = value.trimmed();
1640

1641
  const QString buttonStyle =
1642
      "border-style: none; background: transparent; padding: 0; margin: 0; "
1643
      "icon-size: 16px; color: inherit;";
×
1644

1645
  // Combine the Copy button and the line edit in one widget
1646
  auto *frame = new QFrame();
×
1647
  QLayout *ly = new QHBoxLayout();
×
1648
  ly->setContentsMargins(5, 2, 2, 2);
×
1649
  ly->setSpacing(0);
×
1650
  frame->setLayout(ly);
×
1651
  if (QtPassSettings::getClipBoardType() != Enums::CLIPBOARD_NEVER) {
×
1652
    auto *fieldLabel = new QPushButtonWithClipboard(trimmedValue, this);
×
1653
    connect(fieldLabel, &QPushButtonWithClipboard::clicked, m_qtPass,
×
1654
            &QtPass::copyTextToClipboard);
×
1655

1656
    fieldLabel->setStyleSheet(buttonStyle);
×
1657
    frame->layout()->addWidget(fieldLabel);
×
1658
  }
1659

1660
  if (QtPassSettings::isUseQrencode()) {
×
1661
    auto *qrbutton = new QPushButtonAsQRCode(trimmedValue, this);
×
1662
    connect(qrbutton, &QPushButtonAsQRCode::clicked, m_qtPass,
×
1663
            &QtPass::showTextAsQRCode);
×
1664
    qrbutton->setStyleSheet(buttonStyle);
×
1665
    frame->layout()->addWidget(qrbutton);
×
1666
  }
1667

1668
  // set the echo mode to password, if the field is "password"
1669
  const QString lineStyle =
1670
      QtPassSettings::isUseMonospace()
×
1671
          ? "border-style: none; background: transparent; font-family: "
1672
            "monospace;"
1673
          : "border-style: none; background: transparent;";
×
1674

1675
  if (QtPassSettings::isHidePassword() && trimmedField == tr("Password")) {
×
1676
    auto *line = new QLineEdit();
×
1677
    line->setObjectName(trimmedField);
×
1678
    line->setText(trimmedValue);
×
1679
    line->setReadOnly(true);
×
1680
    line->setStyleSheet(lineStyle);
×
1681
    line->setContentsMargins(0, 0, 0, 0);
×
1682
    line->setEchoMode(QLineEdit::Password);
×
1683
    auto *showButton = new QPushButtonShowPassword(line, this);
×
1684
    showButton->setStyleSheet(buttonStyle);
×
1685
    showButton->setContentsMargins(0, 0, 0, 0);
×
1686
    frame->layout()->addWidget(showButton);
×
1687
    frame->layout()->addWidget(line);
×
1688
  } else {
1689
    auto *line = new QTextBrowser();
×
1690
    line->setOpenExternalLinks(true);
×
1691
    line->setOpenLinks(true);
×
1692
    line->setMaximumHeight(26);
×
1693
    line->setMinimumHeight(26);
×
1694
    line->setSizePolicy(
×
1695
        QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum));
1696
    line->setObjectName(trimmedField);
×
1697
    trimmedValue.replace(Util::protocolRegex(), R"(<a href="\1">\1</a>)");
×
1698
    line->setText(trimmedValue);
×
1699
    line->setReadOnly(true);
×
1700
    line->setStyleSheet(lineStyle);
×
1701
    line->setContentsMargins(0, 0, 0, 0);
×
1702
    frame->layout()->addWidget(line);
×
1703
  }
1704

1705
  frame->setStyleSheet(
×
1706
      ".QFrame{border: 1px solid lightgrey; border-radius: 5px;}");
1707

1708
  // set into the layout
1709
  ui->gridLayout->addWidget(new QLabel(trimmedField), position, 0);
×
1710
  ui->gridLayout->addWidget(frame, position, 1);
×
1711
}
×
1712

1713
/**
1714
 * @brief Displays message in status bar
1715
 *
1716
 * @param msg     text to be displayed
1717
 * @param timeout time for which msg shall be visible
1718
 */
1719
void MainWindow::showStatusMessage(const QString &msg, int timeout) {
×
1720
  ui->statusBar->showMessage(msg, timeout);
×
1721
}
×
1722

1723
/**
1724
 * @brief MainWindow::reencryptPath re-encrypt all passwords in a directory
1725
 * @param dir Directory path to re-encrypt
1726
 */
1727
void MainWindow::reencryptPath(const QString &dir) {
×
1728
  QDir checkDir(dir);
×
1729
  if (!checkDir.exists()) {
×
1730
    QMessageBox::critical(this, tr("Error"),
×
1731
                          tr("Directory does not exist: %1").arg(dir));
×
1732
    return;
×
1733
  }
1734

1735
  int ret = QMessageBox::question(
×
1736
      this, tr("Re-encrypt passwords"),
×
1737
      tr("Re-encrypt all passwords in %1?\n\n"
×
1738
         "This will re-encrypt ALL password files in this folder "
1739
         "using the current recipients defined in .gpg-id.\n\n"
1740
         "This may rewrite many files and cannot be undone easily.\n\n"
1741
         "Continue?")
1742
          .arg(QDir(dir).dirName()),
×
1743
      QMessageBox::Yes | QMessageBox::No);
1744

1745
  if (ret != QMessageBox::Yes)
×
1746
    return;
1747

1748
  // Disable preemptively. ImitatePass::reencryptPath emits
1749
  // startReencryptPath asynchronously and the slot would re-run this,
1750
  // but setEnabled(false) is idempotent so the duplicate is harmless.
1751
  startReencryptPath();
×
1752

1753
  QtPassSettings::getImitatePass()->reencryptPath(
×
1754
      QDir::cleanPath(QDir(dir).absolutePath()));
×
1755
}
×
1756

1757
/**
1758
 * @brief MainWindow::startReencryptPath disable ui elements and treeview
1759
 */
1760
void MainWindow::startReencryptPath() {
×
1761
  setUiElementsEnabled(false);
×
1762
  ui->treeView->setDisabled(true);
×
1763
}
×
1764

1765
/**
1766
 * @brief MainWindow::endReencryptPath re-enable ui elements
1767
 */
1768
void MainWindow::endReencryptPath() { setUiElementsEnabled(true); }
×
1769

1770
/**
1771
 * @brief MainWindow::exportPublicKey export the configured signing key in
1772
 *        ASCII-armored form via gpg and show it in ExportPublicKeyDialog.
1773
 *
1774
 * Falls back to a help dialog when no signing key is configured or gpg is
1775
 * unavailable, so the user still gets actionable guidance.
1776
 */
1777
void MainWindow::exportPublicKey() {
×
1778
  QString identity = QtPassSettings::getPassSigningKey();
×
1779
  if (identity.isEmpty()) {
×
1780
    QMessageBox::information(
×
1781
        this, tr("Export Public Key"),
×
1782
        tr("<h3>Export Your Public Key</h3>"
×
1783
           "<p>No signing key is configured. Set one in QtPass Settings "
1784
           "&gt; GPG keys, or run this in a terminal:</p>"
1785
           "<pre>gpg --armor --export --output my_key.asc &lt;your-key-id"
1786
           "&gt;</pre>"
1787
           "<p>Then send the file to your teammates.</p>"));
1788
    return;
×
1789
  }
1790
  QString gpgExe = QtPassSettings::getGpgExecutable();
×
1791
  if (gpgExe.isEmpty()) {
×
1792
    gpgExe = QStringLiteral("gpg");
×
1793
  }
1794
  QStringList args = {"--armor", "--export"};
×
1795
  args.append(identity.split(' ', Qt::SkipEmptyParts));
×
1796
  QString stdOut;
×
1797
  QString stdErr;
×
1798
  int exitCode =
1799
      Executor::executeBlocking(gpgExe, args, QString(), &stdOut, &stdErr);
×
1800
  if (exitCode != 0 || stdOut.isEmpty()) {
×
1801
    QMessageBox::warning(this, tr("Export Public Key"),
×
1802
                         tr("Could not export public key for %1.\n\n%2")
×
1803
                             .arg(identity, stdErr.isEmpty()
×
1804
                                                ? tr("No output from gpg.")
×
1805
                                                : stdErr));
1806
    return;
1807
  }
1808
  ExportPublicKeyDialog dialog(identity, stdOut, this);
×
1809
  dialog.exec();
×
1810
}
×
1811

1812
/**
1813
 * @brief MainWindow::addRecipient open the recipient management dialog for
1814
 *        the supplied directory.
1815
 * @param dir Folder whose .gpg-id should be edited.
1816
 *
1817
 * Delegates to UsersDialog so users can tick/untick keys from their
1818
 * keyring as recipients of the folder; importing a foreign key into the
1819
 * keyring still has to happen via gpg (or QtPass settings) first.
1820
 */
1821
void MainWindow::addRecipient(const QString &dir) {
×
1822
  UsersDialog d(dir, this);
×
1823
  d.exec();
×
1824
}
×
1825

1826
/**
1827
 * @brief MainWindow::showShareHelp show help about GPG sharing
1828
 */
1829
void MainWindow::showShareHelp() {
×
1830
  QMessageBox::information(
×
1831
      this, tr("Sharing Passwords with GPG"),
×
1832
      tr("<h3>Sharing Passwords with GPG</h3>"
×
1833
         "<p>To share passwords with other users:</p>"
1834
         "<ol>"
1835
         "<li><b>Export your public key</b> and send it to teammates</li>"
1836
         "<li><b>Import teammates' public keys</b> into your GPG keyring</li>"
1837
         "<li><b>Re-encrypt passwords</b> so all recipients can decrypt "
1838
         "them</li>"
1839
         "</ol>"
1840
         "<p>Only people who have a matching secret key can decrypt the "
1841
         "passwords.</p>"
1842
         "<p><b>Tip:</b> Use the same GPG key for all shared folders.</p>"
1843
         "<p>See the FAQ for more details.</p>"));
1844
}
×
1845

1846
void MainWindow::updateGitButtonVisibility() {
×
1847
  if (!QtPassSettings::isUseGit() ||
×
1848
      (QtPassSettings::getGitExecutable().isEmpty() &&
×
1849
       QtPassSettings::getPassExecutable().isEmpty())) {
×
1850
    enableGitButtons(false);
×
1851
  } else {
1852
    enableGitButtons(true);
×
1853
  }
1854
}
×
1855

1856
void MainWindow::updateOtpButtonVisibility() {
×
1857
#if defined(Q_OS_WIN) || defined(__APPLE__)
1858
  ui->actionOtp->setVisible(false);
1859
#endif
1860
  if (!QtPassSettings::isUseOtp()) {
×
1861
    ui->actionOtp->setEnabled(false);
×
1862
  } else {
1863
    ui->actionOtp->setEnabled(true);
×
1864
  }
1865
}
×
1866

1867
void MainWindow::updateGrepButtonVisibility() {
×
1868
  const bool enabled = QtPassSettings::isUseGrepSearch();
×
1869
  ui->grepButton->setVisible(enabled);
×
1870
  ui->grepCaseButton->setVisible(enabled);
×
1871
  if (!enabled && m_grepMode) {
×
1872
    ui->grepButton->setChecked(false);
×
1873
  }
1874
}
×
1875

1876
void MainWindow::enableGitButtons(const bool &state) {
×
1877
  // Following GNOME guidelines is preferable disable buttons instead of hide
1878
  ui->actionPush->setEnabled(state);
×
1879
  ui->actionUpdate->setEnabled(state);
×
1880
}
×
1881

1882
/**
1883
 * @brief MainWindow::critical critical message popup wrapper.
1884
 * @param title
1885
 * @param msg
1886
 */
1887
void MainWindow::critical(const QString &title, const QString &msg) {
×
1888
  QMessageBox::critical(this, title, msg);
×
1889
}
×
1890

1891
/**
1892
 * @brief Appends processed command output to the output panel.
1893
 *
1894
 * Appends text to the process output text edit, with per-line numbering,
1895
 * optional command prefix, and color coding for errors vs. success.
1896
 * Handles auto-scrolling and line limits.
1897
 *
1898
 * @param output The raw output text from the command.
1899
 * @param isError true if this is error output (stderr).
1900
 * @param linePrefix Optional command name to prefix each line with.
1901
 */
1902
void MainWindow::appendProcessOutput(const QString &output, bool isError,
×
1903
                                     const QString &linePrefix) {
1904
  if (!QtPassSettings::isShowProcessOutput()) {
×
1905
    return;
×
1906
  }
1907

1908
  QStringList lines = output.split('\n', Qt::SkipEmptyParts);
×
1909
  for (QString &line : lines) {
×
1910
    // Right-trim only: remove trailing CR and whitespace, preserve leading
1911
    // indentation
1912
    line.remove('\r');
×
1913
    while (!line.isEmpty() && line.back().isSpace()) {
×
1914
      line.chop(1);
×
1915
    }
1916
    if (line.isEmpty()) {
×
1917
      continue;
×
1918
    }
1919

1920
    m_outputCounter++;
×
1921
    QString lineNumber = QString::number(m_outputCounter);
×
1922

1923
    QColor textColor =
1924
        isError ? QColor(Qt::red)
×
1925
                : m_processOutputEdit->palette().color(QPalette::Text);
×
1926
    QString colorHex = textColor.name();
×
1927
    // Apply the optional prefix per line so multi-line output stays
1928
    // attributed to its command (e.g. all 3 lines of a `git push` show
1929
    // "git push: ..." rather than only the first).
1930
    QString prefixed =
1931
        linePrefix.isEmpty() ? line : linePrefix + QStringLiteral(": ") + line;
×
1932
    QString coloredOutput =
1933
        QString("<span style=\"color: %1;\">%2: %3</span>")
×
1934
            .arg(colorHex, lineNumber, prefixed.toHtmlEscaped());
×
1935

1936
    m_processOutputEdit->append(coloredOutput);
×
1937
  }
1938

1939
  limitOutputLines();
×
1940

1941
  if (m_autoScroll) {
×
1942
    m_processOutputEdit->verticalScrollBar()->setValue(
×
1943
        m_processOutputEdit->verticalScrollBar()->maximum());
×
1944
  }
1945
}
1946

1947
/**
1948
 * @brief Handles process output from the Pass executor.
1949
 *
1950
 * Called when any non-sensitive process completes. Filters out password-
1951
 * related commands (pass show, insert, etc.) and delegates to
1952
 * appendProcessOutput.
1953
 *
1954
 * @param output The stdout/stderr text from the process.
1955
 * @param isError true if this is error output (stderr).
1956
 * @param pid The process ID identifying which command ran.
1957
 */
1958
void MainWindow::onProcessOutput(const QString &output, bool isError,
×
1959
                                 Enums::PROCESS pid) {
1960
  appendProcessOutput(output, isError, getProcessName(pid));
×
1961
}
×
1962

1963
/**
1964
 * @brief Maps a process ID to its human-readable command name.
1965
 *
1966
 * Returns static strings for git/pass commands that appear in output.
1967
 * Password-related commands return empty (they are filtered).
1968
 *
1969
 * @param pid The process ID to look up.
1970
 * @return QString with command name, or empty if filtered.
1971
 */
1972
auto MainWindow::getProcessName(Enums::PROCESS pid) -> QString {
×
1973
  switch (pid) {
×
1974
  case Enums::GIT_INIT:
×
1975
    return QStringLiteral("git init"); // no-tr
×
1976
  case Enums::GIT_ADD:
×
1977
    return QStringLiteral("git add"); // no-tr
×
1978
  case Enums::GIT_COMMIT:
×
1979
    return QStringLiteral("git commit"); // no-tr
×
1980
  case Enums::GIT_RM:
×
1981
    return QStringLiteral("git rm"); // no-tr
×
1982
  case Enums::GIT_PULL:
×
1983
    return QStringLiteral("git pull"); // no-tr
×
1984
  case Enums::GIT_PUSH:
×
1985
    return QStringLiteral("git push"); // no-tr
×
1986
  case Enums::GIT_MOVE:
×
1987
    return QStringLiteral("git mv"); // no-tr
×
1988
  case Enums::GIT_COPY:
×
1989
    // ImitatePass::Copy literally invokes `git cp` (a git-extras
1990
    // subcommand), so the label matches what's run. Stock-git users
1991
    // without git-extras will see the underlying "'cp' is not a git
1992
    // command" failure surfaced in the process output panel.
1993
    return QStringLiteral("git cp"); // no-tr
×
1994
  case Enums::PASS_INSERT:
×
1995
    return QStringLiteral("pass insert"); // no-tr
×
1996
  case Enums::PASS_REMOVE:
×
1997
    return QStringLiteral("pass rm"); // no-tr
×
1998
  case Enums::PASS_INIT:
×
1999
    return QStringLiteral("pass init"); // no-tr
×
2000
  case Enums::PASS_MOVE:
×
2001
    return QStringLiteral("pass mv"); // no-tr
×
2002
  case Enums::PASS_COPY:
×
2003
    return QStringLiteral("pass cp"); // no-tr
×
2004
  case Enums::PASS_GREP:
×
2005
    return QStringLiteral("pass grep"); // no-tr
×
2006
  case Enums::GPG_GENKEYS:
×
2007
    return QStringLiteral("gpg --gen-key"); // no-tr
×
2008
  case Enums::PASS_SHOW:
2009
  case Enums::PASS_OTP_GENERATE:
2010
  case Enums::PROCESS_COUNT:
2011
  case Enums::INVALID:
2012
    break;
2013
  }
2014
  return {};
2015
}
2016

2017
/**
2018
 * @brief Checks if a process ID represents a sensitive operation whose
2019
 * output should not be shown in the process output panel.
2020
 *
2021
 * Password-related commands (pass show, OTP generate, grep, insert)
2022
 * display their output in other UI areas, so we skip them here.
2023
 *
2024
 * @param pid The process ID to check.
2025
 * @return true if the process is sensitive and should be filtered.
2026
 */
2027
auto MainWindow::isSensitiveProcess(Enums::PROCESS pid) -> bool {
×
2028
  switch (pid) {
×
2029
  case Enums::PASS_SHOW:
2030
  case Enums::PASS_OTP_GENERATE:
2031
  case Enums::PASS_GREP:
2032
  case Enums::PASS_INSERT:
2033
    return true;
2034
  case Enums::GIT_INIT:
2035
  case Enums::GIT_ADD:
2036
  case Enums::GIT_COMMIT:
2037
  case Enums::GIT_RM:
2038
  case Enums::GIT_PULL:
2039
  case Enums::GIT_PUSH:
2040
  case Enums::GIT_MOVE:
2041
  case Enums::GIT_COPY:
2042
  case Enums::PASS_REMOVE:
2043
  case Enums::PASS_INIT:
2044
  case Enums::PASS_MOVE:
2045
  case Enums::PASS_COPY:
2046
  case Enums::GPG_GENKEYS:
2047
  case Enums::PROCESS_COUNT:
2048
  case Enums::INVALID:
2049
    break;
2050
  }
2051
  return false;
×
2052
}
2053

2054
/**
2055
 * @brief Updates the visibility of the process output panel.
2056
 *
2057
 * Shows or hides the process output widget based on the user's
2058
 * showProcessOutput setting.
2059
 */
2060
void MainWindow::updateProcessOutputVisibility() {
×
2061
  m_processOutputDock->setVisible(QtPassSettings::isShowProcessOutput());
×
2062
}
×
2063

2064
/**
2065
 * @brief Limits the output panel to max lines, trimming old excess.
2066
 *
2067
 * Removes the oldest lines when the document exceeds MaxOutputLines (1000).
2068
 * Called after each append to prevent unbounded growth.
2069
 */
2070
void MainWindow::limitOutputLines() {
×
2071
  QTextDocument *doc = m_processOutputEdit->document();
×
2072
  int excess = doc->blockCount() - MaxOutputLines;
×
2073
  if (excess <= 0) {
×
2074
    return;
×
2075
  }
2076

2077
  QTextCursor cursor(doc);
×
2078
  cursor.movePosition(QTextCursor::Start);
×
2079
  cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, excess);
×
2080
  cursor.removeSelectedText();
×
2081
}
×
2082

2083
/**
2084
 * @brief Clears the process output panel.
2085
 *
2086
 * Clears all output, resets the line counter, and re-enables auto-scroll.
2087
 */
2088
void MainWindow::on_clearOutputButton_clicked() {
×
2089
  m_processOutputEdit->clear();
×
2090
  m_outputCounter = 0;
×
2091
  m_autoScroll = true;
×
2092
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc