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

IJHack / QtPass / 27698290582

17 Jun 2026 02:57PM UTC coverage: 57.126% (-0.002%) from 57.128%
27698290582

Pull #1557

github

web-flow
Merge 096e3b8a8 into db36d9098
Pull Request #1557: refactor(#1511): inject Pass* into StoreModel; inject gpgExe into KeygenDialog

7 of 13 new or added lines in 4 files covered. (53.85%)

1 existing line in 1 file now uncovered.

3964 of 6939 relevant lines covered (57.13%)

23.26 hits per line

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

85.62
/src/storemodel.cpp
1
// SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "storemodel.h"
4
#include "pass.h"
5
#include "pathvalidator.h"
6
#include "util.h"
7
#include <QApplication>
8
#include <QDebug>
9
#include <QFileSystemModel>
10
#include <QMessageBox>
11
#include <QMimeData>
12
#include <QRegularExpression>
13
#include <QtGlobal>
14

15
auto operator<<(
11✔
16
    QDataStream &out,
17
    const dragAndDropInfoPasswordStore &dragAndDropInfoPasswordStore)
18
    -> QDataStream & {
19
  out << static_cast<quint8>(dragAndDropInfoPasswordStore.kind)
11✔
20
      << dragAndDropInfoPasswordStore.path;
11✔
21
  return out;
11✔
22
}
23

24
auto operator>>(QDataStream &in,
14✔
25
                dragAndDropInfoPasswordStore &dragAndDropInfoPasswordStore)
26
    -> QDataStream & {
27
  quint8 k;
28
  in >> k >> dragAndDropInfoPasswordStore.path;
14✔
29
  switch (k) {
14✔
30
  case static_cast<quint8>(dragAndDropInfoPasswordStore::ItemKind::Directory):
3✔
31
    dragAndDropInfoPasswordStore.kind =
3✔
32
        dragAndDropInfoPasswordStore::ItemKind::Directory;
33
    break;
3✔
34
  case static_cast<quint8>(dragAndDropInfoPasswordStore::ItemKind::File):
10✔
35
    dragAndDropInfoPasswordStore.kind =
10✔
36
        dragAndDropInfoPasswordStore::ItemKind::File;
37
    break;
10✔
38
  default:
1✔
39
    dragAndDropInfoPasswordStore.kind =
1✔
40
        dragAndDropInfoPasswordStore::ItemKind::Unknown;
41
    break;
1✔
42
  }
43
  return in;
14✔
44
}
45

46
/**
47
 * @brief StoreModel::StoreModel
48
 * SubClass of QSortFilterProxyModel via
49
 * http://www.qtcentre.org/threads/46471-QTreeView-Filter
50
 */
51
StoreModel::StoreModel() { fs = nullptr; }
47✔
52

53
void StoreModel::setPass(Pass *pass) { m_pass = pass; }
12✔
54

55
/**
56
 * @brief StoreModel::filterAcceptsRow should row be shown, wrapper for
57
 * StoreModel::showThis method.
58
 * @param sourceRow
59
 * @param sourceParent
60
 * @return
61
 */
62
auto StoreModel::filterAcceptsRow(int sourceRow,
118✔
63
                                  const QModelIndex &sourceParent) const
64
    -> bool {
65
  QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
118✔
66
  return showThis(index);
118✔
67
}
68

69
/**
70
 * @brief StoreModel::showThis should a row be shown, based on our search
71
 * criteria.
72
 * @param index
73
 * @return
74
 */
75
auto StoreModel::showThis(const QModelIndex &index) const -> bool {
251✔
76
  bool retVal = false;
77
  if (fs == nullptr) {
251✔
78
    return retVal;
79
  }
80
  // Gives you the info for number of children with a parent
81
  if (sourceModel()->rowCount(index) > 0) {
250✔
82
    for (int nChild = 0; nChild < sourceModel()->rowCount(index); ++nChild) {
132✔
83
      QModelIndex childIndex = sourceModel()->index(nChild, 0, index);
132✔
84
      if (!childIndex.isValid()) {
85
        break;
86
      }
87
      retVal = showThis(childIndex);
132✔
88
      if (retVal) {
132✔
89
        break;
90
      }
91
    }
92
  } else {
93
    QModelIndex useIndex = sourceModel()->index(index.row(), 0, index.parent());
118✔
94
    QString path = fs->filePath(useIndex);
118✔
95
    path = QDir(store).relativeFilePath(path);
236✔
96
    if (path.startsWith(".git")) {
236✔
97
      return false;
98
    }
99
    path.replace(Util::endsWithGpg(), "");
117✔
100
    retVal = path.contains(filterRegularExpression());
117✔
101
  }
102
  return retVal;
103
}
104

105
/**
106
 * @brief StoreModel::setModelAndStore update the source model and store.
107
 * @param sourceModel
108
 * @param passStore
109
 */
110
void StoreModel::setModelAndStore(QFileSystemModel *sourceModel,
41✔
111
                                  const QString &passStore) {
112
  setSourceModel(sourceModel);
41✔
113
  fs = sourceModel;
41✔
114
  store = passStore;
41✔
115
}
41✔
116

117
auto StoreModel::rootIndexFor(const QString &path) -> QModelIndex {
2✔
118
  if (fs == nullptr) {
2✔
119
    return {};
120
  }
121
  return mapFromSource(fs->setRootPath(QDir::cleanPath(path)));
2✔
122
}
123

124
void StoreModel::setStore(const QString &passStore) {
1✔
125
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
126
  // beginFilterChange() is available since Qt 6.9, but the Direction-scoped
127
  // endFilterChange(QSortFilterProxyModel::Direction) overload is only
128
  // available since Qt 6.10, which is the preferred API for scoped and more
129
  // efficient filter updates.
130
  beginFilterChange();
131
  store = passStore;
132
  endFilterChange(QSortFilterProxyModel::Direction::Rows);
133
#else
134
  // Direction-scoped filter changes are unavailable before Qt 6.10, so older
135
  // Qt versions must manually invalidate filters. We update the store and
136
  // manually invalidate the filter as a compatibility path.
137
  store = passStore;
1✔
138
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
139
  QSortFilterProxyModel::invalidateFilter();
1✔
140
#else
141
  invalidateFilter();
142
#endif
143
#endif
144
}
1✔
145

146
/**
147
 * @brief StoreModel::data don't show the .gpg at the end of a file.
148
 * @param index
149
 * @param role
150
 * @return
151
 */
152
auto StoreModel::data(const QModelIndex &index, int role) const -> QVariant {
3✔
153
  if (!index.isValid()) {
154
    return {};
155
  }
156

157
  QVariant initial_value;
158
  initial_value = QSortFilterProxyModel::data(index, role);
2✔
159

160
  if (role == Qt::DisplayRole) {
2✔
161
    QString name = initial_value.toString();
1✔
162
    name.replace(Util::endsWithGpg(), "");
1✔
163
    initial_value.setValue(name);
1✔
164
  }
165

166
  return initial_value;
2✔
167
}
2✔
168

169
/**
170
 * @brief StoreModel::supportedDropActions enable drop.
171
 * @return
172
 */
173
auto StoreModel::supportedDropActions() const -> Qt::DropActions {
1✔
174
  return Qt::CopyAction | Qt::MoveAction;
1✔
175
}
176

177
/**
178
 * @brief StoreModel::supportedDragActions enable drag.
179
 * @return
180
 */
181
auto StoreModel::supportedDragActions() const -> Qt::DropActions {
1✔
182
  return Qt::CopyAction | Qt::MoveAction;
1✔
183
}
184

185
/**
186
 * @brief StoreModel::flags
187
 * @param index
188
 * @return
189
 */
190
auto StoreModel::flags(const QModelIndex &index) const -> Qt::ItemFlags {
2✔
191
  Qt::ItemFlags defaultFlags = QSortFilterProxyModel::flags(index);
2✔
192

193
  if (index.isValid()) {
194
    return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
195
  }
196
  return Qt::ItemIsDropEnabled | defaultFlags;
1✔
197
}
198

199
/**
200
 * @brief StoreModel::mimeTypes
201
 * @return
202
 */
203
auto StoreModel::mimeTypes() const -> QStringList {
1✔
204
  QStringList types;
1✔
205
  types << "application/vnd.qtpass.dragAndDropInfoPasswordStore";
1✔
206
  return types;
1✔
207
}
208

209
/**
210
 * @brief StoreModel::mimeData
211
 * @param indexes
212
 * @return
213
 */
214
auto StoreModel::mimeData(const QModelIndexList &indexes) const -> QMimeData * {
1✔
215
  dragAndDropInfoPasswordStore info;
1✔
216

217
  QByteArray encodedData;
1✔
218
  // only use the first, otherwise we should enable multiselection
219
  QModelIndex index = indexes.at(0);
1✔
220
  if (index.isValid()) {
221
    QModelIndex useIndex = mapToSource(index);
1✔
222
    const QFileInfo fileInfo = fs->fileInfo(useIndex);
1✔
223

224
    if (fileInfo.isDir()) {
1✔
225
      info.kind = dragAndDropInfoPasswordStore::ItemKind::Directory;
×
226
    } else if (fileInfo.isFile()) {
1✔
227
      info.kind = dragAndDropInfoPasswordStore::ItemKind::File;
1✔
228
    }
229
    info.path = fileInfo.absoluteFilePath();
2✔
230
    QDataStream stream(&encodedData, QIODevice::WriteOnly);
1✔
231
    stream << info;
1✔
232
  }
1✔
233

234
  auto *mimeData = new QMimeData();
1✔
235
  mimeData->setData("application/vnd.qtpass.dragAndDropInfoPasswordStore",
2✔
236
                    encodedData);
237
  return mimeData;
1✔
238
}
239

240
/**
241
 * @brief StoreModel::canDropMimeData
242
 * @param data
243
 * @param action
244
 * @param row
245
 * @param column
246
 * @param parent
247
 * @return
248
 */
249
auto StoreModel::canDropMimeData(const QMimeData *data, Qt::DropAction action,
11✔
250
                                 int row, int column,
251
                                 const QModelIndex &parent) const -> bool {
252
#ifdef QT_DEBUG
253
  qDebug() << action << row;
254
#else
255
  Q_UNUSED(action)
256
  Q_UNUSED(row)
257
#endif
258

259
  if (data == nullptr ||
21✔
260
      !data->hasFormat("application/vnd.qtpass.dragAndDropInfoPasswordStore")) {
21✔
261
    return false;
262
  }
263

264
  QByteArray encodedData =
265
      data->data("application/vnd.qtpass.dragAndDropInfoPasswordStore");
18✔
266
  if (encodedData.isEmpty()) {
9✔
267
    return false;
268
  }
269
  QDataStream stream(&encodedData, QIODevice::ReadOnly);
8✔
270
  dragAndDropInfoPasswordStore info;
8✔
271
  stream >> info;
8✔
272
  if (stream.status() != QDataStream::Ok) {
8✔
273
    return false;
274
  }
275

276
  QModelIndex useIndex =
277
      this->index(parent.row(), parent.column(), parent.parent());
8✔
278

279
  if (column > 0) {
8✔
280
    return false;
281
  }
282

283
  using IK = dragAndDropInfoPasswordStore::ItemKind;
284
  // you can drop a folder on a folder
285
  if (fs->fileInfo(mapToSource(useIndex)).isDir() &&
14✔
286
      info.kind == IK::Directory) {
5✔
287
    return true;
288
  }
289
  // you can drop a file on a folder
290
  if (fs->fileInfo(mapToSource(useIndex)).isDir() && info.kind == IK::File) {
8✔
291
    return true;
292
  }
293
  // you can drop a file on a file
294
  if (fs->fileInfo(mapToSource(useIndex)).isFile() && info.kind == IK::File) {
3✔
295
    return true;
296
  }
297

298
  return false;
299
}
8✔
300

301
/**
302
 * @brief StoreModel::dropMimeData
303
 * @param data
304
 * @param action
305
 * @param row
306
 * @param column
307
 * @param parent
308
 * @return
309
 */
310
auto StoreModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
3✔
311
                              int row, int column, const QModelIndex &parent)
312
    -> bool {
313
  if (!canDropMimeData(data, action, row, column, parent)) {
3✔
314
    return false;
315
  }
316

317
  if (action == Qt::IgnoreAction) {
3✔
318
    return true;
319
  }
320

321
  if (action != Qt::MoveAction && action != Qt::CopyAction) {
3✔
322
    return false;
323
  }
324

325
  dragAndDropInfoPasswordStore info;
3✔
326
  if (!parseDropData(data, &info)) {
3✔
327
    return false;
328
  }
329

330
  return executeDropAction(info, action, parent);
3✔
331
}
332

333
auto StoreModel::parseDropData(const QMimeData *data,
3✔
334
                               dragAndDropInfoPasswordStore *outInfo) -> bool {
335
  QByteArray encodedData =
336
      data->data("application/vnd.qtpass.dragAndDropInfoPasswordStore");
6✔
337
  if (encodedData.isEmpty()) {
3✔
338
    return false;
339
  }
340

341
  QDataStream stream(&encodedData, QIODevice::ReadOnly);
3✔
342
  dragAndDropInfoPasswordStore info;
3✔
343
  stream >> info;
3✔
344
  if (stream.status() != QDataStream::Ok) {
3✔
345
    return false;
346
  }
347

348
  *outInfo = info;
349
  return true;
3✔
350
}
3✔
351

352
auto StoreModel::executeDropAction(const dragAndDropInfoPasswordStore &info,
3✔
353
                                   Qt::DropAction action,
354
                                   const QModelIndex &parent) -> bool {
355
  QModelIndex destIndex =
356
      this->index(parent.row(), parent.column(), parent.parent());
3✔
357
  QFileInfo destFileinfo = fs->fileInfo(mapToSource(destIndex));
3✔
358
  QFileInfo srcFileInfo = QFileInfo(info.path);
3✔
359

360
  QString cleanedSrc = QDir::cleanPath(srcFileInfo.absoluteFilePath());
3✔
361
  QString cleanedDest = QDir::cleanPath(destFileinfo.absoluteFilePath());
3✔
362

363
  // Both endpoints must resolve inside the password store after symlink
364
  // resolution. Drop data is encoded by the dragged item but could be
365
  // crafted; canonical-path checks stop drops that would move/copy outside
366
  // the store or follow a symlink out (e.g. a symlink within the store
367
  // pointing at /etc).
368
  if (!PathValidator::isPathInStore(store, cleanedSrc) ||
3✔
NEW
369
      !PathValidator::isPathInStore(store, cleanedDest)) {
×
370
    qWarning() << "executeDropAction: rejecting drop that escapes the store"
6✔
371
               << "(src=" << cleanedSrc << "dest=" << cleanedDest << ")";
3✔
372
    return false;
3✔
373
  }
374

375
  switch (info.kind) {
×
376
  case dragAndDropInfoPasswordStore::ItemKind::Directory: {
×
377
    // Dropping a folder onto a folder: move/copy it *into* the target.
378
    if (!destFileinfo.isDir()) {
×
379
      return false;
380
    }
381
    const QString destDir =
382
        QDir::cleanPath(QDir(cleanedDest).filePath(srcFileInfo.fileName()));
×
383
    return performDrop(cleanedSrc, destDir, action, false);
×
384
  }
385
  case dragAndDropInfoPasswordStore::ItemKind::File:
×
386
    // File onto a folder drops into it (no clash); file onto an existing
387
    // file asks before overwriting.
388
    if (destFileinfo.isDir()) {
×
389
      return performDrop(cleanedSrc, cleanedDest, action, false);
×
390
    }
391
    return performDrop(
×
392
        cleanedSrc, cleanedDest, action,
393
        QMessageBox::question(
×
394
            qobject_cast<QWidget *>(QObject::parent()), tr("Force overwrite?"),
×
395
            tr("overwrite %1 with %2?").arg(cleanedDest, cleanedSrc),
×
396
            QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes);
397
  default:
398
    qWarning() << "executeDropAction: unexpected ItemKind, ignoring drop";
×
399
    return false;
×
400
  }
401
}
3✔
402

403
auto StoreModel::performDrop(const QString &cleanedSrc,
×
404
                             const QString &cleanedDest, Qt::DropAction action,
405
                             bool force) -> bool {
NEW
406
  if (!m_pass) {
×
407
    return false;
408
  }
409
  if (action == Qt::MoveAction) {
×
NEW
410
    m_pass->Move(cleanedSrc, cleanedDest, force);
×
411
  } else if (action == Qt::CopyAction) {
×
NEW
412
    m_pass->Copy(cleanedSrc, cleanedDest, force);
×
413
  }
414
  return true;
415
}
416

417
/**
418
 * @brief StoreModel::lessThan
419
 * @param source_left
420
 * @param source_right
421
 * @return
422
 */
423
auto StoreModel::lessThan(const QModelIndex &source_left,
2✔
424
                          const QModelIndex &source_right) const -> bool {
425
/* matches logic in QFileSystemModelSorter::compareNodes() */
426
#ifndef Q_OS_MAC
427
  if (fs && (source_left.column() == 0 || source_left.column() == 1)) {
2✔
428
    bool leftD = fs->isDir(source_left);
2✔
429
    bool rightD = fs->isDir(source_right);
2✔
430

431
    if (leftD ^ rightD) {
2✔
432
      return leftD;
433
    }
434
  }
435
#endif
436

437
  return QSortFilterProxyModel::lessThan(source_left, source_right);
1✔
438
}
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