keepassxc

Форк
0
/
EntryAttachmentsWidget.cpp 
454 строки · 15.6 Кб
1
/*
2
 *  Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
3
 *
4
 *  This program is free software: you can redistribute it and/or modify
5
 *  it under the terms of the GNU General Public License as published by
6
 *  the Free Software Foundation, either version 2 or (at your option)
7
 *  version 3 of the License.
8
 *
9
 *  This program is distributed in the hope that it will be useful,
10
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 *  GNU General Public License for more details.
13
 *
14
 *  You should have received a copy of the GNU General Public License
15
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
 */
17

18
#include "EntryAttachmentsWidget.h"
19
#include "ui_EntryAttachmentsWidget.h"
20

21
#include <QDir>
22
#include <QDropEvent>
23
#include <QMimeData>
24
#include <QStandardPaths>
25
#include <QTemporaryFile>
26

27
#include "EntryAttachmentsModel.h"
28
#include "core/Config.h"
29
#include "core/EntryAttachments.h"
30
#include "core/Tools.h"
31
#include "gui/FileDialog.h"
32
#include "gui/MessageBox.h"
33

34
EntryAttachmentsWidget::EntryAttachmentsWidget(QWidget* parent)
35
    : QWidget(parent)
36
    , m_ui(new Ui::EntryAttachmentsWidget)
37
    , m_entryAttachments(nullptr)
38
    , m_attachmentsModel(new EntryAttachmentsModel(this))
39
    , m_readOnly(false)
40
    , m_buttonsVisible(true)
41
{
42
    m_ui->setupUi(this);
43

44
    m_ui->attachmentsView->setAcceptDrops(false);
45
    m_ui->attachmentsView->viewport()->setAcceptDrops(true);
46
    m_ui->attachmentsView->viewport()->installEventFilter(this);
47

48
    m_ui->attachmentsView->setModel(m_attachmentsModel);
49
    m_ui->attachmentsView->verticalHeader()->hide();
50
    m_ui->attachmentsView->horizontalHeader()->setStretchLastSection(true);
51
    m_ui->attachmentsView->horizontalHeader()->resizeSection(EntryAttachmentsModel::NameColumn, 400);
52
    m_ui->attachmentsView->setSelectionBehavior(QAbstractItemView::SelectRows);
53
    m_ui->attachmentsView->setSelectionMode(QAbstractItemView::ExtendedSelection);
54
    m_ui->attachmentsView->setEditTriggers(QAbstractItemView::SelectedClicked);
55

56
    connect(this, SIGNAL(buttonsVisibleChanged(bool)), this, SLOT(updateButtonsVisible()));
57
    connect(this, SIGNAL(readOnlyChanged(bool)), SLOT(updateButtonsEnabled()));
58
    connect(m_attachmentsModel, SIGNAL(modelReset()), SLOT(updateButtonsEnabled()));
59

60
    // clang-format off
61
    connect(m_ui->attachmentsView->selectionModel(),
62
            SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
63
            SLOT(updateButtonsEnabled()));
64
    // clang-format on
65
    connect(this, SIGNAL(readOnlyChanged(bool)), m_attachmentsModel, SLOT(setReadOnly(bool)));
66

67
    connect(m_ui->attachmentsView, SIGNAL(doubleClicked(QModelIndex)), SLOT(openAttachment(QModelIndex)));
68
    connect(m_ui->saveAttachmentButton, SIGNAL(clicked()), SLOT(saveSelectedAttachments()));
69
    connect(m_ui->openAttachmentButton, SIGNAL(clicked()), SLOT(openSelectedAttachments()));
70
    connect(m_ui->addAttachmentButton, SIGNAL(clicked()), SLOT(insertAttachments()));
71
    connect(m_ui->removeAttachmentButton, SIGNAL(clicked()), SLOT(removeSelectedAttachments()));
72
    connect(m_ui->renameAttachmentButton, SIGNAL(clicked()), SLOT(renameSelectedAttachments()));
73

74
    updateButtonsVisible();
75
    updateButtonsEnabled();
76
}
77

78
EntryAttachmentsWidget::~EntryAttachmentsWidget() = default;
79

80
const EntryAttachments* EntryAttachmentsWidget::attachments() const
81
{
82
    return m_entryAttachments;
83
}
84

85
bool EntryAttachmentsWidget::isReadOnly() const
86
{
87
    return m_readOnly;
88
}
89

90
bool EntryAttachmentsWidget::isButtonsVisible() const
91
{
92
    return m_buttonsVisible;
93
}
94

95
void EntryAttachmentsWidget::linkAttachments(EntryAttachments* attachments)
96
{
97
    unlinkAttachments();
98

99
    m_entryAttachments = attachments;
100
    m_attachmentsModel->setEntryAttachments(m_entryAttachments);
101

102
    if (m_entryAttachments) {
103
        connect(m_entryAttachments,
104
                SIGNAL(valueModifiedExternally(QString, QString)),
105
                this,
106
                SLOT(attachmentModifiedExternally(QString, QString)));
107
        connect(m_entryAttachments, SIGNAL(modified()), this, SIGNAL(widgetUpdated()));
108
    }
109
}
110

111
void EntryAttachmentsWidget::unlinkAttachments()
112
{
113
    if (m_entryAttachments) {
114
        m_entryAttachments->disconnect(this);
115
        m_entryAttachments = nullptr;
116
        m_attachmentsModel->setEntryAttachments(nullptr);
117
    }
118
}
119

120
void EntryAttachmentsWidget::setReadOnly(bool readOnly)
121
{
122
    if (m_readOnly == readOnly) {
123
        return;
124
    }
125

126
    m_readOnly = readOnly;
127
    emit readOnlyChanged(m_readOnly);
128
}
129

130
void EntryAttachmentsWidget::setButtonsVisible(bool buttonsVisible)
131
{
132
    if (m_buttonsVisible == buttonsVisible) {
133
        return;
134
    }
135

136
    m_buttonsVisible = buttonsVisible;
137
    emit buttonsVisibleChanged(m_buttonsVisible);
138
}
139

140
void EntryAttachmentsWidget::insertAttachments()
141
{
142
    Q_ASSERT(m_entryAttachments);
143
    Q_ASSERT(!isReadOnly());
144
    if (isReadOnly()) {
145
        return;
146
    }
147

148
    QString defaultDirPath = FileDialog::getLastDir("attachments");
149
    const auto filenames = fileDialog()->getOpenFileNames(this, tr("Select files"), defaultDirPath);
150
    if (filenames.isEmpty()) {
151
        return;
152
    }
153
    const auto confirmedFileNames = confirmAttachmentSelection(filenames);
154
    if (confirmedFileNames.isEmpty()) {
155
        return;
156
    }
157
    // Save path to first filename
158
    FileDialog::saveLastDir("attachments", filenames[0]);
159
    QString errorMessage;
160
    if (!insertAttachments(confirmedFileNames, errorMessage)) {
161
        errorOccurred(errorMessage);
162
    }
163
    emit widgetUpdated();
164
}
165

166
void EntryAttachmentsWidget::removeSelectedAttachments()
167
{
168
    Q_ASSERT(m_entryAttachments);
169
    Q_ASSERT(!isReadOnly());
170
    if (isReadOnly()) {
171
        return;
172
    }
173

174
    const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0);
175
    if (indexes.isEmpty()) {
176
        return;
177
    }
178

179
    auto result = MessageBox::question(this,
180
                                       tr("Confirm remove"),
181
                                       tr("Are you sure you want to remove %n attachment(s)?", "", indexes.count()),
182
                                       MessageBox::Remove | MessageBox::Cancel,
183
                                       MessageBox::Cancel);
184

185
    if (result == MessageBox::Remove) {
186
        QStringList keys;
187
        for (const QModelIndex& index : indexes) {
188
            keys.append(m_attachmentsModel->keyByIndex(index));
189
        }
190
        m_entryAttachments->remove(keys);
191
        emit widgetUpdated();
192
    }
193
}
194

195
void EntryAttachmentsWidget::renameSelectedAttachments()
196
{
197
    Q_ASSERT(m_entryAttachments);
198
    m_ui->attachmentsView->edit(m_ui->attachmentsView->selectionModel()->selectedIndexes().first());
199
}
200

201
void EntryAttachmentsWidget::saveSelectedAttachments()
202
{
203
    Q_ASSERT(m_entryAttachments);
204

205
    const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0);
206
    if (indexes.isEmpty()) {
207
        return;
208
    }
209

210
    QString defaultDirPath = FileDialog::getLastDir("attachments");
211
    const QString saveDirPath = fileDialog()->getExistingDirectory(this, tr("Save attachments"), defaultDirPath);
212
    if (saveDirPath.isEmpty()) {
213
        return;
214
    }
215

216
    QDir saveDir(saveDirPath);
217
    if (!saveDir.exists()) {
218
        if (saveDir.mkpath(saveDir.absolutePath())) {
219
            errorOccurred(tr("Unable to create directory:\n%1").arg(saveDir.absolutePath()));
220
            return;
221
        }
222
    }
223
    FileDialog::saveLastDir("attachments", saveDirPath);
224

225
    QStringList errors;
226
    for (const QModelIndex& index : indexes) {
227
        const QString filename = m_attachmentsModel->keyByIndex(index);
228
        const QString attachmentPath = saveDir.absoluteFilePath(filename);
229

230
        if (QFileInfo::exists(attachmentPath)) {
231

232
            MessageBox::Buttons buttons = MessageBox::Overwrite | MessageBox::Cancel;
233
            if (indexes.length() > 1) {
234
                buttons |= MessageBox::Skip;
235
            }
236

237
            const QString questionText(
238
                tr("Are you sure you want to overwrite the existing file \"%1\" with the attachment?"));
239

240
            auto result = MessageBox::question(
241
                this, tr("Confirm overwrite"), questionText.arg(filename), buttons, MessageBox::Cancel);
242

243
            if (result == MessageBox::Skip) {
244
                continue;
245
            } else if (result == MessageBox::Cancel) {
246
                return;
247
            }
248
        }
249

250
        QFile file(attachmentPath);
251
        const QByteArray attachmentData = m_entryAttachments->value(filename);
252
        const bool saveOk = file.open(QIODevice::WriteOnly) && file.setPermissions(QFile::ReadUser | QFile::WriteUser)
253
                            && file.write(attachmentData) == attachmentData.size();
254
        if (!saveOk) {
255
            errors.append(QString("%1 - %2").arg(filename, file.errorString()));
256
        }
257
    }
258

259
    if (!errors.isEmpty()) {
260
        errorOccurred(tr("Unable to save attachments:\n%1").arg(errors.join('\n')));
261
    }
262
}
263

264
void EntryAttachmentsWidget::openAttachment(const QModelIndex& index)
265
{
266
    Q_ASSERT(index.isValid());
267
    if (!index.isValid()) {
268
        return;
269
    }
270

271
    QString errorMessage;
272
    if (!m_entryAttachments->openAttachment(m_attachmentsModel->keyByIndex(index), &errorMessage)) {
273
        errorOccurred(tr("Unable to open attachment:\n%1").arg(errorMessage));
274
    }
275
}
276

277
void EntryAttachmentsWidget::openSelectedAttachments()
278
{
279
    const QModelIndexList indexes = m_ui->attachmentsView->selectionModel()->selectedRows(0);
280
    if (indexes.isEmpty()) {
281
        return;
282
    }
283

284
    QStringList errors;
285
    for (const QModelIndex& index : indexes) {
286
        QString errorMessage;
287
        if (!m_entryAttachments->openAttachment(m_attachmentsModel->keyByIndex(index), &errorMessage)) {
288
            const QString filename = m_attachmentsModel->keyByIndex(index);
289
            errors.append(QString("%1 - %2").arg(filename, errorMessage));
290
        };
291
    }
292

293
    if (!errors.isEmpty()) {
294
        errorOccurred(tr("Unable to open attachments:\n%1").arg(errors.join('\n')));
295
    }
296
}
297

298
void EntryAttachmentsWidget::updateButtonsEnabled()
299
{
300
    const bool hasSelection = m_ui->attachmentsView->selectionModel()->hasSelection();
301

302
    m_ui->addAttachmentButton->setEnabled(!m_readOnly);
303
    m_ui->removeAttachmentButton->setEnabled(hasSelection && !m_readOnly);
304
    m_ui->renameAttachmentButton->setEnabled(hasSelection && !m_readOnly);
305

306
    m_ui->saveAttachmentButton->setEnabled(hasSelection);
307
    m_ui->openAttachmentButton->setEnabled(hasSelection);
308
}
309

310
void EntryAttachmentsWidget::updateButtonsVisible()
311
{
312
    m_ui->addAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
313
    m_ui->removeAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
314
    m_ui->renameAttachmentButton->setVisible(m_buttonsVisible && !m_readOnly);
315
}
316

317
bool EntryAttachmentsWidget::insertAttachments(const QStringList& filenames, QString& errorMessage)
318
{
319
    Q_ASSERT(!isReadOnly());
320
    if (isReadOnly()) {
321
        return false;
322
    }
323

324
    QStringList errors;
325
    for (const QString& filename : filenames) {
326
        QByteArray data;
327
        QFile file(filename);
328
        const QFileInfo fInfo(filename);
329
        const bool readOk = file.open(QIODevice::ReadOnly) && Tools::readAllFromDevice(&file, data);
330
        if (readOk) {
331
            m_entryAttachments->set(fInfo.fileName(), data);
332
        } else {
333
            errors.append(QString("%1 - %2").arg(fInfo.fileName(), file.errorString()));
334
        }
335
    }
336

337
    if (!errors.isEmpty()) {
338
        errorMessage = tr("Unable to open file(s):\n%1", "", errors.size()).arg(errors.join('\n'));
339
    }
340

341
    return errors.isEmpty();
342
}
343

344
QStringList EntryAttachmentsWidget::confirmAttachmentSelection(const QStringList& filenames)
345
{
346
    QStringList confirmedFileNames;
347
    for (const auto& file : filenames) {
348
        const QFileInfo fileInfo(file);
349
        auto fileName = fileInfo.fileName();
350

351
        // Ask for confirmation if overwriting an existing attachment
352
        if (m_entryAttachments->hasKey(fileName)) {
353
            auto result = MessageBox::question(this,
354
                                               tr("Confirm Overwrite Attachment"),
355
                                               tr("Attachment \"%1\" already exists. \n"
356
                                                  "Would you like to overwrite the existing attachment?")
357
                                                   .arg(fileName),
358
                                               MessageBox::Overwrite | MessageBox::No,
359
                                               MessageBox::No);
360
            if (result == MessageBox::No) {
361
                continue;
362
            }
363
        }
364

365
        // Ask for confirmation before adding files over 5 MB in size
366
        double size = fileInfo.size() / (1024.0 * 1024.0);
367
        if (size > 5.0) {
368
            auto result =
369
                MessageBox::question(this,
370
                                     tr("Confirm Attachment"),
371
                                     tr("%1 is a big file (%2 MB).\nYour database may get very large and reduce "
372
                                        "performance.\n\nAre you sure to add this file?")
373
                                         .arg(fileName, QString::number(size, 'f', 1)),
374
                                     MessageBox::Yes | MessageBox::No,
375
                                     MessageBox::No);
376
            if (result == MessageBox::No) {
377
                continue;
378
            }
379
        }
380

381
        confirmedFileNames << file;
382
    }
383

384
    return confirmedFileNames;
385
}
386

387
bool EntryAttachmentsWidget::eventFilter(QObject* watched, QEvent* e)
388
{
389
    if (watched == m_ui->attachmentsView->viewport() && !isReadOnly()) {
390
        const QEvent::Type eventType = e->type();
391
        if (eventType == QEvent::DragEnter || eventType == QEvent::DragMove) {
392
            auto dropEv = static_cast<QDropEvent*>(e);
393
            const QMimeData* mimeData = dropEv->mimeData();
394
            if (mimeData->hasUrls()) {
395
                dropEv->acceptProposedAction();
396
                return true;
397
            }
398
        } else if (eventType == QEvent::Drop) {
399
            auto dropEv = static_cast<QDropEvent*>(e);
400
            const QMimeData* mimeData = dropEv->mimeData();
401
            if (mimeData->hasUrls()) {
402
                dropEv->acceptProposedAction();
403
                QStringList filenames;
404
                const QList<QUrl> urls = mimeData->urls();
405
                for (const QUrl& url : urls) {
406
                    const QFileInfo fInfo(url.toLocalFile());
407
                    if (fInfo.isFile()) {
408
                        filenames.append(fInfo.absoluteFilePath());
409
                    }
410
                }
411

412
                QString errorMessage;
413
                if (!insertAttachments(filenames, errorMessage)) {
414
                    errorOccurred(errorMessage);
415
                }
416

417
                return true;
418
            }
419
        }
420
    }
421

422
    return QWidget::eventFilter(watched, e);
423
}
424

425
void EntryAttachmentsWidget::attachmentModifiedExternally(const QString& key, const QString& filePath)
426
{
427
    if (m_pendingChanges.contains(filePath)) {
428
        return;
429
    }
430

431
    m_pendingChanges << filePath;
432

433
    auto result = MessageBox::question(
434
        this,
435
        tr("Attachment modified"),
436
        tr("The attachment '%1' was modified.\nDo you want to save the changes to your database?").arg(key),
437
        MessageBox::Save | MessageBox::Discard,
438
        MessageBox::Save);
439

440
    if (result == MessageBox::Save) {
441
        QFile f(filePath);
442
        if (f.open(QFile::ReadOnly)) {
443
            m_entryAttachments->set(key, f.readAll());
444
            f.close();
445
            emit widgetUpdated();
446
        } else {
447
            MessageBox::critical(this,
448
                                 tr("Saving attachment failed"),
449
                                 tr("Saving updated attachment failed.\nError: %1").arg(f.errorString()));
450
        }
451
    }
452

453
    m_pendingChanges.removeAll(filePath);
454
}
455

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.