Line data Source code
1 : // SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief Implements StoreModel and the StoreDragItem stream operators; the
6 : * drop validation lives in executeDropAction().
7 : */
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 :
22 16 : auto operator<<(QDataStream &out, const StoreDragItem &item) -> QDataStream & {
23 16 : out << static_cast<quint8>(item.kind) << item.path;
24 16 : return out;
25 : }
26 :
27 23 : auto operator>>(QDataStream &in, StoreDragItem &item) -> QDataStream & {
28 : quint8 k;
29 23 : in >> k >> item.path;
30 23 : switch (k) {
31 5 : case static_cast<quint8>(StoreDragItem::ItemKind::Directory):
32 5 : item.kind = StoreDragItem::ItemKind::Directory;
33 5 : break;
34 17 : case static_cast<quint8>(StoreDragItem::ItemKind::File):
35 17 : item.kind = StoreDragItem::ItemKind::File;
36 17 : break;
37 1 : default:
38 1 : item.kind = StoreDragItem::ItemKind::Unknown;
39 1 : break;
40 : }
41 23 : return in;
42 : }
43 :
44 : // Filter approach via http://www.qtcentre.org/threads/46471-QTreeView-Filter
45 174 : StoreModel::StoreModel() { fs = nullptr; }
46 :
47 137 : void StoreModel::setPass(Pass *pass) { m_pass = pass; }
48 :
49 4642 : auto StoreModel::filterAcceptsRow(int sourceRow,
50 : const QModelIndex &sourceParent) const
51 : -> bool {
52 4642 : QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
53 4642 : return showThis(index);
54 : }
55 :
56 7029 : auto StoreModel::showThis(const QModelIndex &index) const -> bool {
57 : bool retVal = false;
58 7029 : if (fs == nullptr) {
59 : return retVal;
60 : }
61 6892 : if (sourceModel()->rowCount(index) > 0) {
62 2480 : for (int nChild = 0; nChild < sourceModel()->rowCount(index); ++nChild) {
63 2386 : QModelIndex childIndex = sourceModel()->index(nChild, 0, index);
64 : if (!childIndex.isValid()) {
65 : break;
66 : }
67 2386 : retVal = showThis(childIndex);
68 2386 : if (retVal) {
69 : break;
70 : }
71 : }
72 : } else {
73 5379 : QModelIndex useIndex = sourceModel()->index(index.row(), 0, index.parent());
74 5379 : QString path = fs->filePath(useIndex);
75 10758 : path = QDir(store).relativeFilePath(path);
76 10758 : if (path.startsWith(".git")) {
77 : return false;
78 : }
79 5378 : path.replace(Util::endsWithGpg(), "");
80 5378 : retVal = path.contains(filterRegularExpression());
81 : }
82 : return retVal;
83 : }
84 :
85 168 : void StoreModel::setModelAndStore(QFileSystemModel *sourceModel,
86 : const QString &passStore) {
87 168 : setSourceModel(sourceModel);
88 168 : fs = sourceModel;
89 168 : store = passStore;
90 168 : }
91 :
92 196 : auto StoreModel::rootIndexFor(const QString &path) -> QModelIndex {
93 196 : if (fs == nullptr) {
94 1 : return {};
95 : }
96 390 : return mapFromSource(fs->setRootPath(QDir::cleanPath(path)));
97 : }
98 :
99 10 : void 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 10 : store = passStore;
107 10 : invalidateFilter();
108 : #endif
109 10 : }
110 :
111 15800 : auto StoreModel::data(const QModelIndex &index, int role) const -> QVariant {
112 : if (!index.isValid()) {
113 : return {};
114 : }
115 :
116 : QVariant initial_value;
117 15799 : initial_value = QSortFilterProxyModel::data(index, role);
118 :
119 15799 : if (role == Qt::DisplayRole) {
120 2076 : QString name = initial_value.toString();
121 2076 : name.replace(Util::endsWithGpg(), "");
122 2076 : initial_value.setValue(name);
123 : }
124 :
125 : return initial_value;
126 15799 : }
127 :
128 1 : auto StoreModel::supportedDropActions() const -> Qt::DropActions {
129 1 : return Qt::CopyAction | Qt::MoveAction;
130 : }
131 :
132 1 : auto StoreModel::supportedDragActions() const -> Qt::DropActions {
133 1 : return Qt::CopyAction | Qt::MoveAction;
134 : }
135 :
136 7047 : auto StoreModel::flags(const QModelIndex &index) const -> Qt::ItemFlags {
137 7047 : Qt::ItemFlags defaultFlags = QSortFilterProxyModel::flags(index);
138 :
139 : if (index.isValid()) {
140 : return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
141 : }
142 1 : return Qt::ItemIsDropEnabled | defaultFlags;
143 : }
144 :
145 1 : auto StoreModel::mimeTypes() const -> QStringList {
146 1 : QStringList types;
147 0 : types << kStoreDragMimeType;
148 1 : return types;
149 : }
150 :
151 1 : auto StoreModel::mimeData(const QModelIndexList &indexes) const -> QMimeData * {
152 1 : StoreDragItem info;
153 :
154 1 : if (indexes.isEmpty())
155 : return nullptr;
156 1 : QByteArray encodedData;
157 : // only use the first, otherwise we should enable multiselection
158 1 : QModelIndex index = indexes.at(0);
159 : if (index.isValid()) {
160 1 : QModelIndex useIndex = mapToSource(index);
161 1 : const QFileInfo fileInfo = fs->fileInfo(useIndex);
162 :
163 1 : if (fileInfo.isDir()) {
164 0 : info.kind = StoreDragItem::ItemKind::Directory;
165 1 : } else if (fileInfo.isFile()) {
166 1 : info.kind = StoreDragItem::ItemKind::File;
167 : }
168 1 : info.path = fileInfo.absoluteFilePath();
169 1 : QDataStream stream(&encodedData, QIODevice::WriteOnly);
170 1 : stream << info;
171 1 : }
172 :
173 1 : auto *mimeData = new QMimeData();
174 2 : mimeData->setData(kStoreDragMimeType, encodedData);
175 : return mimeData;
176 : }
177 :
178 16 : auto StoreModel::canDropMimeData(const QMimeData *data, Qt::DropAction action,
179 : int row, int column,
180 : const QModelIndex &parent) const -> bool {
181 16 : qCDebug(lcQtPass) << "canDropMimeData" << action << row;
182 :
183 16 : const auto parsed = parseDropData(data);
184 16 : if (!parsed) {
185 : return false;
186 : }
187 : const StoreDragItem &info = *parsed;
188 :
189 : QModelIndex useIndex =
190 13 : this->index(parent.row(), parent.column(), parent.parent());
191 :
192 13 : if (column > 0) {
193 : return false;
194 : }
195 :
196 : using IK = StoreDragItem::ItemKind;
197 : // you can drop a folder on a folder
198 24 : if (fs->fileInfo(mapToSource(useIndex)).isDir() &&
199 10 : info.kind == IK::Directory) {
200 : return true;
201 : }
202 : // you can drop a file on a folder
203 12 : if (fs->fileInfo(mapToSource(useIndex)).isDir() && info.kind == IK::File) {
204 : return true;
205 : }
206 : // you can drop a file on a file
207 3 : if (fs->fileInfo(mapToSource(useIndex)).isFile() && info.kind == IK::File) {
208 : return true;
209 : }
210 :
211 : return false;
212 : }
213 :
214 8 : auto StoreModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
215 : int row, int column, const QModelIndex &parent)
216 : -> bool {
217 8 : if (!canDropMimeData(data, action, row, column, parent)) {
218 : return false;
219 : }
220 :
221 8 : if (action == Qt::IgnoreAction) {
222 : return true;
223 : }
224 :
225 7 : if (action != Qt::MoveAction && action != Qt::CopyAction) {
226 : return false;
227 : }
228 :
229 7 : const auto info = parseDropData(data);
230 7 : if (!info) {
231 : return false;
232 : }
233 :
234 7 : return executeDropAction(*info, action, parent);
235 : }
236 :
237 23 : auto StoreModel::parseDropData(const QMimeData *data)
238 : -> std::optional<StoreDragItem> {
239 45 : if (data == nullptr || !data->hasFormat(kStoreDragMimeType)) {
240 2 : return std::nullopt;
241 : }
242 42 : QByteArray encodedData = data->data(kStoreDragMimeType);
243 21 : if (encodedData.isEmpty()) {
244 1 : return std::nullopt;
245 : }
246 20 : QDataStream stream(&encodedData, QIODevice::ReadOnly);
247 20 : StoreDragItem info;
248 20 : stream >> info;
249 20 : if (stream.status() != QDataStream::Ok) {
250 0 : return std::nullopt;
251 : }
252 : return info;
253 20 : }
254 :
255 7 : auto StoreModel::executeDropAction(const StoreDragItem &info,
256 : Qt::DropAction action,
257 : const QModelIndex &parent) -> bool {
258 : QModelIndex destIndex =
259 7 : this->index(parent.row(), parent.column(), parent.parent());
260 7 : QFileInfo destFileinfo = fs->fileInfo(mapToSource(destIndex));
261 7 : QFileInfo srcFileInfo = QFileInfo(info.path);
262 :
263 7 : QString cleanedSrc = QDir::cleanPath(srcFileInfo.absoluteFilePath());
264 7 : 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 11 : if (!PathValidator::isPathInStore(store, cleanedSrc) ||
269 4 : !PathValidator::isPathInStore(store, cleanedDest)) {
270 6 : qCWarning(lcQtPass)
271 3 : << "executeDropAction: rejecting drop that escapes the store"
272 3 : << "(src=" << cleanedSrc << "dest=" << cleanedDest << ")";
273 3 : return false;
274 : }
275 :
276 4 : switch (info.kind) {
277 1 : case StoreDragItem::ItemKind::Directory: {
278 : // Dropping a folder onto a folder: move/copy it *into* the target.
279 1 : if (!destFileinfo.isDir()) {
280 : return false;
281 : }
282 : const QString destDir =
283 2 : QDir::cleanPath(QDir(cleanedDest).filePath(srcFileInfo.fileName()));
284 1 : return performDrop(cleanedSrc, destDir, action, false);
285 : }
286 3 : case StoreDragItem::ItemKind::File:
287 : // File onto a folder drops into it (no clash); file onto an existing
288 : // file asks before overwriting.
289 3 : if (destFileinfo.isDir()) {
290 3 : return performDrop(cleanedSrc, cleanedDest, action, false);
291 : }
292 0 : return performDrop(
293 : cleanedSrc, cleanedDest, action,
294 0 : PlainMessageBox::askYes(
295 0 : qobject_cast<QWidget *>(QObject::parent()), tr("Force overwrite?"),
296 0 : tr("overwrite %1 with %2?").arg(cleanedDest, cleanedSrc)));
297 0 : default:
298 0 : qCWarning(lcQtPass)
299 0 : << "executeDropAction: unexpected ItemKind, ignoring drop";
300 0 : return false;
301 : }
302 7 : }
303 :
304 4 : auto StoreModel::performDrop(const QString &cleanedSrc,
305 : const QString &cleanedDest, Qt::DropAction action,
306 : bool force) -> bool {
307 4 : if (!m_pass) {
308 : return false;
309 : }
310 3 : if (action == Qt::MoveAction) {
311 6 : m_pass->Move(cleanedSrc, cleanedDest, force);
312 1 : } else if (action == Qt::CopyAction) {
313 3 : m_pass->Copy(cleanedSrc, cleanedDest, force);
314 : }
315 : return true;
316 : }
317 :
318 8946 : auto 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 8946 : if (fs && (source_left.column() == 0 || source_left.column() == 1)) {
323 8946 : bool leftD = fs->isDir(source_left);
324 8946 : bool rightD = fs->isDir(source_right);
325 :
326 8946 : if (leftD ^ rightD) {
327 : return leftD;
328 : }
329 : }
330 : #endif
331 :
332 7217 : return QSortFilterProxyModel::lessThan(source_left, source_right);
333 : }
|