QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
storemodel.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
8#include "storemodel.h"
9#include "pass.h"
10#include "pathvalidator.h"
11#include "plainmessagebox.h"
12#include "qtpasslogging.h"
13#include "util.h"
14#include <QApplication>
15#include <QDebug>
16#include <QFileSystemModel>
17#include <QMessageBox>
18#include <QMimeData>
19#include <QRegularExpression>
20#include <QtGlobal>
21
22auto operator<<(QDataStream &out, const StoreDragItem &item) -> QDataStream & {
23 out << static_cast<quint8>(item.kind) << item.path;
24 return out;
25}
26
27auto operator>>(QDataStream &in, StoreDragItem &item) -> QDataStream & {
28 quint8 k;
29 in >> k >> item.path;
30 switch (k) {
31 case static_cast<quint8>(StoreDragItem::ItemKind::Directory):
33 break;
34 case static_cast<quint8>(StoreDragItem::ItemKind::File):
36 break;
37 default:
39 break;
40 }
41 return in;
42}
43
44// Filter approach via http://www.qtcentre.org/threads/46471-QTreeView-Filter
45StoreModel::StoreModel() { fs = nullptr; }
46
47void StoreModel::setPass(Pass *pass) { m_pass = pass; }
48
50 const QModelIndex &sourceParent) const
51 -> bool {
52 QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
53 return showThis(index);
54}
55
56auto StoreModel::showThis(const QModelIndex &index) const -> bool {
57 bool retVal = false;
58 if (fs == nullptr) {
59 return retVal;
60 }
61 if (sourceModel()->rowCount(index) > 0) {
62 for (int nChild = 0; nChild < sourceModel()->rowCount(index); ++nChild) {
63 QModelIndex childIndex = sourceModel()->index(nChild, 0, index);
64 if (!childIndex.isValid()) {
65 break;
66 }
67 retVal = showThis(childIndex);
68 if (retVal) {
69 break;
70 }
71 }
72 } else {
73 QModelIndex useIndex = sourceModel()->index(index.row(), 0, index.parent());
74 QString path = fs->filePath(useIndex);
75 path = QDir(store).relativeFilePath(path);
76 if (path.startsWith(".git")) {
77 return false;
78 }
79 path.replace(Util::endsWithGpg(), "");
80 retVal = path.contains(filterRegularExpression());
81 }
82 return retVal;
83}
84
85void StoreModel::setModelAndStore(QFileSystemModel *sourceModel,
86 const QString &passStore) {
87 setSourceModel(sourceModel);
88 fs = sourceModel;
89 store = passStore;
90}
91
92auto StoreModel::rootIndexFor(const QString &path) -> QModelIndex {
93 if (fs == nullptr) {
94 return {};
95 }
96 return mapFromSource(fs->setRootPath(QDir::cleanPath(path)));
97}
98
99void StoreModel::setStore(const QString &passStore) {
100#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
101 // The Direction-scoped endFilterChange() overload needs Qt 6.10.
102 beginFilterChange();
103 store = passStore;
104 endFilterChange(QSortFilterProxyModel::Direction::Rows);
105#else
106 store = passStore;
107 invalidateFilter();
108#endif
109}
110
111auto StoreModel::data(const QModelIndex &index, int role) const -> QVariant {
112 if (!index.isValid()) {
113 return {};
114 }
115
116 QVariant initial_value;
117 initial_value = QSortFilterProxyModel::data(index, role);
118
119 if (role == Qt::DisplayRole) {
120 QString name = initial_value.toString();
121 name.replace(Util::endsWithGpg(), "");
122 initial_value.setValue(name);
123 }
124
125 return initial_value;
126}
127
128auto StoreModel::supportedDropActions() const -> Qt::DropActions {
129 return Qt::CopyAction | Qt::MoveAction;
130}
131
132auto StoreModel::supportedDragActions() const -> Qt::DropActions {
133 return Qt::CopyAction | Qt::MoveAction;
134}
135
136auto StoreModel::flags(const QModelIndex &index) const -> Qt::ItemFlags {
137 Qt::ItemFlags defaultFlags = QSortFilterProxyModel::flags(index);
138
139 if (index.isValid()) {
140 return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
141 }
142 return Qt::ItemIsDropEnabled | defaultFlags;
143}
144
145auto StoreModel::mimeTypes() const -> QStringList {
146 QStringList types;
147 types << kStoreDragMimeType;
148 return types;
149}
150
151auto StoreModel::mimeData(const QModelIndexList &indexes) const -> QMimeData * {
152 StoreDragItem info;
153
154 if (indexes.isEmpty())
155 return nullptr;
156 QByteArray encodedData;
157 // only use the first, otherwise we should enable multiselection
158 QModelIndex index = indexes.at(0);
159 if (index.isValid()) {
160 QModelIndex useIndex = mapToSource(index);
161 const QFileInfo fileInfo = fs->fileInfo(useIndex);
162
163 if (fileInfo.isDir()) {
165 } else if (fileInfo.isFile()) {
167 }
168 info.path = fileInfo.absoluteFilePath();
169 QDataStream stream(&encodedData, QIODevice::WriteOnly);
170 stream << info;
171 }
172
173 auto *mimeData = new QMimeData();
174 mimeData->setData(kStoreDragMimeType, encodedData);
175 return mimeData;
176}
177
178auto StoreModel::canDropMimeData(const QMimeData *data, Qt::DropAction action,
179 int row, int column,
180 const QModelIndex &parent) const -> bool {
181 qCDebug(lcQtPass) << "canDropMimeData" << action << row;
182
183 const auto parsed = parseDropData(data);
184 if (!parsed) {
185 return false;
186 }
187 const StoreDragItem &info = *parsed;
188
189 QModelIndex useIndex =
190 this->index(parent.row(), parent.column(), parent.parent());
191
192 if (column > 0) {
193 return false;
194 }
195
196 using IK = StoreDragItem::ItemKind;
197 // you can drop a folder on a folder
198 if (fs->fileInfo(mapToSource(useIndex)).isDir() &&
199 info.kind == IK::Directory) {
200 return true;
201 }
202 // you can drop a file on a folder
203 if (fs->fileInfo(mapToSource(useIndex)).isDir() && info.kind == IK::File) {
204 return true;
205 }
206 // you can drop a file on a file
207 if (fs->fileInfo(mapToSource(useIndex)).isFile() && info.kind == IK::File) {
208 return true;
209 }
210
211 return false;
212}
213
214auto StoreModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
215 int row, int column, const QModelIndex &parent)
216 -> bool {
217 if (!canDropMimeData(data, action, row, column, parent)) {
218 return false;
219 }
220
221 if (action == Qt::IgnoreAction) {
222 return true;
223 }
224
225 if (action != Qt::MoveAction && action != Qt::CopyAction) {
226 return false;
227 }
228
229 const auto info = parseDropData(data);
230 if (!info) {
231 return false;
232 }
233
234 return executeDropAction(*info, action, parent);
235}
236
237auto StoreModel::parseDropData(const QMimeData *data)
238 -> std::optional<StoreDragItem> {
239 if (data == nullptr || !data->hasFormat(kStoreDragMimeType)) {
240 return std::nullopt;
241 }
242 QByteArray encodedData = data->data(kStoreDragMimeType);
243 if (encodedData.isEmpty()) {
244 return std::nullopt;
245 }
246 QDataStream stream(&encodedData, QIODevice::ReadOnly);
247 StoreDragItem info;
248 stream >> info;
249 if (stream.status() != QDataStream::Ok) {
250 return std::nullopt;
251 }
252 return info;
253}
254
256 Qt::DropAction action,
257 const QModelIndex &parent) -> bool {
258 QModelIndex destIndex =
259 this->index(parent.row(), parent.column(), parent.parent());
260 QFileInfo destFileinfo = fs->fileInfo(mapToSource(destIndex));
261 QFileInfo srcFileInfo = QFileInfo(info.path);
262
263 QString cleanedSrc = QDir::cleanPath(srcFileInfo.absoluteFilePath());
264 QString cleanedDest = QDir::cleanPath(destFileinfo.absoluteFilePath());
265
266 // Drop data could be crafted: both endpoints must resolve inside the store
267 // after symlink resolution (e.g. no in-store symlink pointing at /etc).
268 if (!PathValidator::isPathInStore(store, cleanedSrc) ||
269 !PathValidator::isPathInStore(store, cleanedDest)) {
270 qCWarning(lcQtPass)
271 << "executeDropAction: rejecting drop that escapes the store"
272 << "(src=" << cleanedSrc << "dest=" << cleanedDest << ")";
273 return false;
274 }
275
276 switch (info.kind) {
278 // Dropping a folder onto a folder: move/copy it *into* the target.
279 if (!destFileinfo.isDir()) {
280 return false;
281 }
282 const QString destDir =
283 QDir::cleanPath(QDir(cleanedDest).filePath(srcFileInfo.fileName()));
284 return performDrop(cleanedSrc, destDir, action, false);
285 }
287 // File onto a folder drops into it (no clash); file onto an existing
288 // file asks before overwriting.
289 if (destFileinfo.isDir()) {
290 return performDrop(cleanedSrc, cleanedDest, action, false);
291 }
292 return performDrop(
293 cleanedSrc, cleanedDest, action,
295 qobject_cast<QWidget *>(QObject::parent()), tr("Force overwrite?"),
296 tr("overwrite %1 with %2?").arg(cleanedDest, cleanedSrc)));
297 default:
298 qCWarning(lcQtPass)
299 << "executeDropAction: unexpected ItemKind, ignoring drop";
300 return false;
301 }
302}
303
304auto StoreModel::performDrop(const QString &cleanedSrc,
305 const QString &cleanedDest, Qt::DropAction action,
306 bool force) -> bool {
307 if (!m_pass) {
308 return false;
309 }
310 if (action == Qt::MoveAction) {
311 m_pass->Move(cleanedSrc, cleanedDest, force);
312 } else if (action == Qt::CopyAction) {
313 m_pass->Copy(cleanedSrc, cleanedDest, force);
314 }
315 return true;
316}
317
318auto StoreModel::lessThan(const QModelIndex &source_left,
319 const QModelIndex &source_right) const -> bool {
320/* matches logic in QFileSystemModelSorter::compareNodes() */
321#ifndef Q_OS_MAC
322 if (fs && (source_left.column() == 0 || source_left.column() == 1)) {
323 bool leftD = fs->isDir(source_left);
324 bool rightD = fs->isDir(source_right);
325
326 if (leftD ^ rightD) {
327 return leftD;
328 }
329 }
330#endif
331
332 return QSortFilterProxyModel::lessThan(source_left, source_right);
333}
Abstract base class for password store operations.
Definition pass.h:75
static auto isPathInStore(const QString &storeRoot, const QString &candidate) -> bool
Check that a candidate path resolves inside the password store root, after following symlinks and res...
auto filterAcceptsRow(int source_row, const QModelIndex &source_parent) const -> bool override
Filter whether a row should be displayed.
auto data(const QModelIndex &index, int role) const -> QVariant override
Get display data for index.
StoreModel()
Construct a StoreModel.
static auto parseDropData(const QMimeData *data) -> std::optional< StoreDragItem >
Decode the drag payload from data.
auto showThis(const QModelIndex &index) const -> bool
Check if a specific index should be shown.
auto mimeTypes() const -> QStringList override
Get supported MIME types for drag/drop.
Pass * m_pass
Backend that performs drag-and-drop Move/Copy; not owned, null until setPass(), in which case drops a...
Definition storemodel.h:102
void setModelAndStore(QFileSystemModel *sourceModel, const QString &passStore)
Initialize model with source model and store path.
auto dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) -> bool override
Handle dropped MIME data.
void setStore(const QString &passStore)
Update the store path used for filtering without changing the source model.
auto mimeData(const QModelIndexList &indexes) const -> QMimeData *override
Create MIME data from indexes.
auto executeDropAction(const StoreDragItem &info, Qt::DropAction action, const QModelIndex &parent) -> bool
Resolve a validated drop into a Move or Copy.
void setPass(Pass *pass)
Inject the Pass backend for drag-and-drop Move/Copy operations.
auto rootIndexFor(const QString &path) -> QModelIndex
Set the source filesystem root and map it to a proxy index.
QString store
Store root used to make paths relative for filtering and for the drop boundary check.
Definition storemodel.h:97
auto supportedDragActions() const -> Qt::DropActions override
Get supported drag actions.
auto flags(const QModelIndex &index) const -> Qt::ItemFlags override
Get item flags for index.
auto performDrop(const QString &cleanedSrc, const QString &cleanedDest, Qt::DropAction action, bool force) -> bool
Perform the actual Move/Copy for a validated drop.
auto canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const -> bool override
Check if drop is possible.
auto lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const -> bool override
Compare two indices for sorting.
QFileSystemModel * fs
The wrapped file system model, set by setModelAndStore(); null until then, which makes showThis() hid...
Definition storemodel.h:92
auto supportedDropActions() const -> Qt::DropActions override
Get supported drop actions.
static auto endsWithGpg() -> const QRegularExpression &
Returns a regex to match .gpg file extensions.
Definition util.cpp:582
auto askYes(QWidget *parent, const QString &title, const QString &text) -> bool
Ask a yes/no question in plain text.
Pass, the abstract password-store backend that RealPass and ImitatePass implement,...
PathValidator: is a path inside the password store, and how is it spelled relative to the store root.
QMessageBox helpers that force Qt::PlainText so store-controlled names can never be read as rich-text...
The "qtpass" logging category.
auto operator>>(QDataStream &in, StoreDragItem &item) -> QDataStream &
Deserialize a StoreDragItem from the QtPass MIME type.
auto operator<<(QDataStream &out, const StoreDragItem &item) -> QDataStream &
Serialize a StoreDragItem for the QtPass MIME type.
StoreModel, the filtering/sorting proxy over the store's QFileSystemModel, and StoreDragItem,...
What a drag from the store tree carries: the kind of the dragged item and its path.
Definition storemodel.h:28
QString path
Definition storemodel.h:39
ItemKind
Type of the dragged item.
Definition storemodel.h:32
ItemKind kind
Definition storemodel.h:38
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...