jobs.cpp 25.5 KB
Newer Older
1
2
/*
 * Copyright (c) 2007 Henrique Pinto <henrique.pinto@kdemail.net>
3
 * Copyright (c) 2008-2009 Harald Hvaal <haraldhv@stud.ntnu.no>
4
 * Copyright (c) 2009-2012 Raphael Kubo da Costa <rakuco@FreeBSD.org>
5
 * Copyright (c) 2016 Vladyslav Batyrenko <mvlabat@gmail.com>
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
28

29
#include "jobs.h"
30
#include "ark_debug.h"
31

32
#include <QDir>
33
#include <QDirIterator>
34
#include <QFileInfo>
35
#include <QThread>
36
#include <QTimer>
Elvis Angelaccio's avatar
Elvis Angelaccio committed
37
#include <QUrl>
38

39
#include <KFileUtils>
40
#include <KLocalizedString>
41

42
43
namespace Kerfuffle
{
44

45
46
class Job::Private : public QThread
{
Elvis Angelaccio's avatar
Elvis Angelaccio committed
47
48
    Q_OBJECT

49
public:
Elvis Angelaccio's avatar
Elvis Angelaccio committed
50
    Private(Job *job, QObject *parent = nullptr)
51
52
53
54
55
        : QThread(parent)
        , q(job)
    {
    }

56
    void run() override;
57

58
private:
59
60
61
62
63
    Job *q;
};

void Job::Private::run()
{
64
    q->doWork();
65
66
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
67
Job::Job(Archive *archive, ReadOnlyArchiveInterface *interface)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
68
    : KJob()
Elvis Angelaccio's avatar
Elvis Angelaccio committed
69
    , m_archive(archive)
70
    , m_archiveInterface(interface)
71
    , d(new Private(this))
72
73
74
75
{
    setCapabilities(KJob::Killable);
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
76
Job::Job(Archive *archive)
77
    : Job(archive, nullptr)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
78
79
80
{}

Job::Job(ReadOnlyArchiveInterface *interface)
81
    : Job(nullptr, interface)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
82
83
{}

84
85
Job::~Job()
{
86
87
    if (d->isRunning()) {
        d->wait();
88
89
    }

90
    delete d;
91
}
92

93
94
ReadOnlyArchiveInterface *Job::archiveInterface()
{
Elvis Angelaccio's avatar
Elvis Angelaccio committed
95
96
97
98
99
100
    // Use the archive interface.
    if (archive()) {
        return archive()->interface();
    }

    // Use the interface passed to this job (e.g. JSONArchiveInterface in jobstest.cpp).
101
102
103
    return m_archiveInterface;
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
Archive *Job::archive() const
{
    return m_archive;
}

QString Job::errorString() const
{
    if (!errorText().isEmpty()) {
        return errorText();
    }

    if (archive()) {
        if (archive()->error() == NoPlugin) {
            return i18n("No suitable plugin found. Ark does not seem to support this file type.");
        }

        if (archive()->error() == FailedPlugin) {
            return i18n("Failed to load a suitable plugin. Make sure any executables needed to handle the archive type are installed.");
        }
    }

    return QString();
}

128
129
void Job::start()
{
130
    jobTimer.start();
131

Elvis Angelaccio's avatar
Elvis Angelaccio committed
132
133
134
135
136
137
138
139
    // We have an archive but it's not valid, nothing to do.
    if (archive() && !archive()->isValid()) {
        QTimer::singleShot(0, this, [=]() {
            onFinished(false);
        });
        return;
    }

140
    if (archiveInterface()->waitForFinishedSignal()) {
141
142
143
144
145
146
        // CLI-based interfaces run a QProcess, no need to use threads.
        QTimer::singleShot(0, this, &Job::doWork);
    } else {
        // Run the job in another thread.
        d->start();
    }
147
148
}

149
150
void Job::connectToArchiveInterfaceSignals()
{
Laurent Montel's avatar
Laurent Montel committed
151
152
153
154
155
    connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &Job::onCancelled);
    connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &Job::onError);
    connect(archiveInterface(), &ReadOnlyArchiveInterface::entry, this, &Job::onEntry);
    connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &Job::onProgress);
    connect(archiveInterface(), &ReadOnlyArchiveInterface::info, this, &Job::onInfo);
156
    connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished);
Laurent Montel's avatar
Laurent Montel committed
157
    connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery);
158
159
160
161
162

    auto readWriteInterface = qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());
    if (readWriteInterface) {
        connect(readWriteInterface, &ReadWriteArchiveInterface::entryRemoved, this, &Job::onEntryRemoved);
    }
163
164
}

165
166
void Job::onCancelled()
{
167
    qCDebug(ARK) << "Cancelled emitted";
168
169
170
    setError(KJob::KilledJobError);
}

171
void Job::onError(const QString & message, const QString & details, int errorCode)
172
{
173
    Q_UNUSED(details)
174

175
176
    qCDebug(ARK) << "Error emitted:" << errorCode << "-" << message;
    setError(errorCode);
177
178
179
    setErrorText(message);
}

180
void Job::onEntry(Archive::Entry *entry)
181
{
182
183
184
    const QString entryFullPath = entry->fullPath();
    if (QDir::cleanPath(entryFullPath).contains(QLatin1String("../"))) {
        qCWarning(ARK) << "Possibly malicious archive. Detected entry that could lead to a directory traversal attack:" << entryFullPath;
185
        onError(i18n("Could not load the archive because it contains ill-formed entries and might be a malicious archive."), QString(), Kerfuffle::PossiblyMaliciousArchiveError);
186
187
188
189
        onFinished(false);
        return;
    }

Alexander Lohnau's avatar
Alexander Lohnau committed
190
    Q_EMIT newEntry(entry);
191
192
193
194
195
196
197
198
199
}

void Job::onProgress(double value)
{
    setPercent(static_cast<unsigned long>(100.0*value));
}

void Job::onInfo(const QString& info)
{
Alexander Lohnau's avatar
Alexander Lohnau committed
200
    Q_EMIT infoMessage(this, info);
201
202
203
204
}

void Job::onEntryRemoved(const QString & path)
{
Alexander Lohnau's avatar
Alexander Lohnau committed
205
    Q_EMIT entryRemoved(path);
206
207
208
209
}

void Job::onFinished(bool result)
{
210
    qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms";
211

Elvis Angelaccio's avatar
Elvis Angelaccio committed
212
213
214
215
    if (archive() && !archive()->isValid()) {
        setError(KJob::UserDefinedError);
    }

216
217
218
    if (!d->isInterruptionRequested()) {
        emitResult();
    }
219
220
221
222
}

void Job::onUserQuery(Query *query)
{
223
224
225
226
    if (archiveInterface()->waitForFinishedSignal()) {
        qCWarning(ARK) << "Plugins run from the main thread should call directly query->execute()";
    }

Alexander Lohnau's avatar
Alexander Lohnau committed
227
    Q_EMIT userQuery(query);
228
229
230
231
}

bool Job::doKill()
{
Elvis Angelaccio's avatar
Elvis Angelaccio committed
232
233
234
    const bool killed = archiveInterface()->doKill();
    if (killed) {
        return true;
235
236
    }

Elvis Angelaccio's avatar
Elvis Angelaccio committed
237
238
239
    if (d->isRunning()) {
        qCDebug(ARK) << "Requesting graceful thread interruption, will abort in one second otherwise.";
        d->requestInterruption();
Elvis Angelaccio's avatar
Elvis Angelaccio committed
240
        d->wait(1000);
241
    }
Elvis Angelaccio's avatar
Elvis Angelaccio committed
242

Elvis Angelaccio's avatar
Elvis Angelaccio committed
243
    return true;
244
245
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
246
247
LoadJob::LoadJob(Archive *archive, ReadOnlyArchiveInterface *interface)
    : Job(archive, interface)
248
249
250
    , m_isSingleFolderArchive(true)
    , m_isPasswordProtected(false)
    , m_extractedFilesSize(0)
251
252
    , m_dirCount(0)
    , m_filesCount(0)
253
{
254
    qCDebug(ARK) << "Created job instance";
Elvis Angelaccio's avatar
Elvis Angelaccio committed
255
    connect(this, &LoadJob::newEntry, this, &LoadJob::onNewEntry);
256
257
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
258
LoadJob::LoadJob(Archive *archive)
259
    : LoadJob(archive, nullptr)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
260
261
262
{}

LoadJob::LoadJob(ReadOnlyArchiveInterface *interface)
263
    : LoadJob(nullptr, interface)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
264
265
266
{}

void LoadJob::doWork()
267
{
Alexander Lohnau's avatar
Alexander Lohnau committed
268
    Q_EMIT description(this, i18n("Loading archive"), qMakePair(i18n("Archive"), archiveInterface()->filename()));
269
    connectToArchiveInterfaceSignals();
Elvis Angelaccio's avatar
Elvis Angelaccio committed
270

271
    bool ret = archiveInterface()->list();
272

273
    if (!archiveInterface()->waitForFinishedSignal()) {
274
275
276
277
278
        // onFinished() needs to be called after onNewEntry(), because the former reads members set in the latter.
        // So we need to put it in the event queue, just like the single-thread case does by emitting finished().
        QTimer::singleShot(0, this, [=]() {
            onFinished(ret);
        });
279
    }
280
281
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
282
283
void LoadJob::onFinished(bool result)
{
284
    if (archive() && result) {
Elvis Angelaccio's avatar
Elvis Angelaccio committed
285
286
287
288
289
290
291
292
293
294
295
296
297
        archive()->setProperty("unpackedSize", extractedFilesSize());
        archive()->setProperty("isSingleFolder", isSingleFolderArchive());
        const auto name = subfolderName().isEmpty() ? archive()->completeBaseName() : subfolderName();
        archive()->setProperty("subfolderName", name);
        if (isPasswordProtected()) {
            archive()->setProperty("encryptionType",  archive()->password().isEmpty() ? Archive::Encrypted : Archive::HeaderEncrypted);
        }
    }

    Job::onFinished(result);
}

qlonglong LoadJob::extractedFilesSize() const
298
299
300
301
{
    return m_extractedFilesSize;
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
302
bool LoadJob::isPasswordProtected() const
303
304
305
306
{
    return m_isPasswordProtected;
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
307
bool LoadJob::isSingleFolderArchive() const
308
{
309
310
311
312
    if (m_filesCount == 1 && m_dirCount == 0) {
        return false;
    }

313
314
315
    return m_isSingleFolderArchive;
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
316
void LoadJob::onNewEntry(const Archive::Entry *entry)
317
{
318
319
    m_extractedFilesSize += entry->property("size").toLongLong();
    m_isPasswordProtected |= entry->property("isPasswordProtected").toBool();
320

321
    if (entry->isDir()) {
322
323
324
325
326
        m_dirCount++;
    } else {
        m_filesCount++;
    }

327
    if (m_isSingleFolderArchive) {
Nicolas Fella's avatar
Nicolas Fella committed
328
        QString fullPath = entry->fullPath();
329
        // RPM filenames have the ./ prefix, and "." would be detected as the subfolder name, so we remove it.
Nicolas Fella's avatar
Nicolas Fella committed
330
331
332
333
        if (fullPath.startsWith(QLatin1String("./"))) {
            fullPath = fullPath.remove(0, 2);
        }

334
        const QString basePath = fullPath.split(QLatin1Char('/')).at(0);
335

336
337
338
        if (m_basePath.isEmpty()) {
            m_basePath = basePath;
            m_subfolderName = basePath;
339
        } else {
340
341
342
343
            if (m_basePath != basePath) {
                m_isSingleFolderArchive = false;
                m_subfolderName.clear();
            }
344
345
346
347
        }
    }
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
348
QString LoadJob::subfolderName() const
349
{
350
351
352
353
    if (!isSingleFolderArchive()) {
        return QString();
    }

354
355
356
    return m_subfolderName;
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
357
358
359
360
361
362
363
BatchExtractJob::BatchExtractJob(LoadJob *loadJob, const QString &destination, bool autoSubfolder, bool preservePaths)
    : Job(loadJob->archive())
    , m_loadJob(loadJob)
    , m_destination(destination)
    , m_autoSubfolder(autoSubfolder)
    , m_preservePaths(preservePaths)
{
364
    qCDebug(ARK) << "Created job instance";
Elvis Angelaccio's avatar
Elvis Angelaccio committed
365
366
367
368
369
}

void BatchExtractJob::doWork()
{
    connect(m_loadJob, &KJob::result, this, &BatchExtractJob::slotLoadingFinished);
370
371
    connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &BatchExtractJob::onCancelled);

372
373
374
375
    if (archiveInterface()->hasBatchExtractionProgress()) {
        // progress() will be actually emitted by the LoadJob, but the archiveInterface() is the same.
        connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotLoadingProgress);
    }
Elvis Angelaccio's avatar
Elvis Angelaccio committed
376

377
    // Forward LoadJob's signals.
Elvis Angelaccio's avatar
Elvis Angelaccio committed
378
379
380
381
382
    connect(m_loadJob, &Kerfuffle::Job::newEntry, this, &BatchExtractJob::newEntry);
    connect(m_loadJob, &Kerfuffle::Job::userQuery, this, &BatchExtractJob::userQuery);
    m_loadJob->start();
}

383
384
385
386
387
388
389
390
391
bool BatchExtractJob::doKill()
{
    if (m_step == Loading) {
        return m_loadJob->kill();
    }

    return m_extractJob->kill();
}

392
393
394
395
396
397
398
399
400
401
402
403
404
void BatchExtractJob::slotLoadingProgress(double progress)
{
    // Progress from LoadJob counts only for 50% of the BatchExtractJob's duration.
    m_lastPercentage = static_cast<unsigned long>(50.0*progress);
    setPercent(m_lastPercentage);
}

void BatchExtractJob::slotExtractProgress(double progress)
{
    // The 2nd 50% of the BatchExtractJob's duration comes from the ExtractJob.
    setPercent(m_lastPercentage + static_cast<unsigned long>(50.0*progress));
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
405
406
407
void BatchExtractJob::slotLoadingFinished(KJob *job)
{
    if (job->error()) {
408
        // Forward errors as well.
409
        onError(job->errorString(), QString(), job->error());
410
        onFinished(false);
Elvis Angelaccio's avatar
Elvis Angelaccio committed
411
412
413
414
415
416
417
        return;
    }

    // Now we can start extraction.
    setupDestination();

    Kerfuffle::ExtractionOptions options;
418
    options.setPreservePaths(m_preservePaths);
Elvis Angelaccio's avatar
Elvis Angelaccio committed
419

420
421
422
423
    m_extractJob = archive()->extractFiles({}, m_destination, options);
    if (m_extractJob) {
        connect(m_extractJob, &KJob::result, this, &BatchExtractJob::emitResult);
        connect(m_extractJob, &Kerfuffle::Job::userQuery, this, &BatchExtractJob::userQuery);
424
        connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &BatchExtractJob::onError);
425
426
427
428
429
        if (archiveInterface()->hasBatchExtractionProgress()) {
            // The LoadJob is done, change slot and start setting the percentage from m_lastPercentage on.
            disconnect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotLoadingProgress);
            connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotExtractProgress);
        }
430
431
        m_step = Extracting;
        m_extractJob->start();
Elvis Angelaccio's avatar
Elvis Angelaccio committed
432
433
434
435
436
437
438
439
440
441
    } else {
        emitResult();
    }
}

void BatchExtractJob::setupDestination()
{
    const bool isSingleFolderRPM = (archive()->isSingleFolder() &&
                                   (archive()->mimeType().name() == QLatin1String("application/x-rpm")));

442
    if (m_autoSubfolder && (archive()->hasMultipleTopLevelEntries() || isSingleFolderRPM)) {
Elvis Angelaccio's avatar
Elvis Angelaccio committed
443
444
445
446
447
        const QDir d(m_destination);
        QString subfolderName = archive()->subfolderName();

        // Special case for single folder RPM archives.
        // We don't want the autodetected folder to have a meaningless "usr" name.
448
        if (isSingleFolderRPM && subfolderName == QLatin1String("usr")) {
Elvis Angelaccio's avatar
Elvis Angelaccio committed
449
450
451
452
453
            qCDebug(ARK) << "Detected single folder RPM archive. Using archive basename as subfolder name";
            subfolderName = QFileInfo(archive()->fileName()).completeBaseName();
        }

        if (d.exists(subfolderName)) {
454
            subfolderName = KFileUtils::suggestName(QUrl::fromUserInput(m_destination, QString(), QUrl::AssumeLocalFile), subfolderName);
Elvis Angelaccio's avatar
Elvis Angelaccio committed
455
456
457
458
459
460
461
462
        }

        d.mkdir(subfolderName);

        m_destination += QLatin1Char( '/' ) + subfolderName;
    }
}

463
CreateJob::CreateJob(Archive *archive, const QVector<Archive::Entry*> &entries, const CompressionOptions &options)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
464
465
466
467
    : Job(archive)
    , m_entries(entries)
    , m_options(options)
{
468
    qCDebug(ARK) << "Created job instance";
Elvis Angelaccio's avatar
Elvis Angelaccio committed
469
470
471
472
473
474
475
476
477
478
479
480
481
482
}

void CreateJob::enableEncryption(const QString &password, bool encryptHeader)
{
    archive()->encrypt(password, encryptHeader);
}

void CreateJob::setMultiVolume(bool isMultiVolume)
{
    archive()->setMultiVolume(isMultiVolume);
}

void CreateJob::doWork()
{
483
484
    connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &CreateJob::onProgress);

Elvis Angelaccio's avatar
Elvis Angelaccio committed
485
    m_addJob = archive()->addFiles(m_entries, nullptr, m_options);
Elvis Angelaccio's avatar
Elvis Angelaccio committed
486

487
488
    if (m_addJob) {
        connect(m_addJob, &KJob::result, this, &CreateJob::emitResult);
489
        // Forward description signal from AddJob, we need to change the first argument ('this' needs to be a CreateJob).
490
        connect(m_addJob, &KJob::description, this, [=](KJob *, const QString &title, const QPair<QString,QString> &field1, const QPair<QString,QString> &) {
Alexander Lohnau's avatar
Alexander Lohnau committed
491
            Q_EMIT description(this, title, field1);
492
493
        });

494
        m_addJob->start();
Elvis Angelaccio's avatar
Elvis Angelaccio committed
495
496
497
498
499
    } else {
        emitResult();
    }
}

500
501
502
503
504
bool CreateJob::doKill()
{
    return m_addJob && m_addJob->kill();
}

Laurent Montel's avatar
Laurent Montel committed
505
ExtractJob::ExtractJob(const QVector<Archive::Entry*> &entries, const QString &destinationDir, ExtractionOptions options, ReadOnlyArchiveInterface *interface)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
506
    : Job(interface)
507
    , m_entries(entries)
508
509
    , m_destinationDir(destinationDir)
    , m_options(options)
510
{
511
    qCDebug(ARK) << "Created job instance";
512
513
514
515
516
}

void ExtractJob::doWork()
{
    QString desc;
517
    if (m_entries.count() == 0) {
518
519
        desc = i18n("Extracting all files");
    } else {
520
        desc = i18np("Extracting one file", "Extracting %1 files", m_entries.count());
521
    }
Alexander Lohnau's avatar
Alexander Lohnau committed
522
    Q_EMIT description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename()), qMakePair(i18nc("extraction folder", "Destination"), m_destinationDir));
523

524
525
    QFileInfo destDirInfo(m_destinationDir);
    if (destDirInfo.isDir() && (!destDirInfo.isWritable() || !destDirInfo.isExecutable())) {
526
        onError(xi18n("Could not write to destination <filename>%1</filename>.<nl/>Check whether you have sufficient permissions.", m_destinationDir), QString(), Kerfuffle::DestinationNotWritableError);
527
528
529
530
        onFinished(false);
        return;
    }

531
    connectToArchiveInterfaceSignals();
532

533
    qCDebug(ARK) << "Starting extraction with" << m_entries.count() << "selected files."
534
             << m_entries
535
536
             << "Destination dir:" << m_destinationDir
             << "Options:" << m_options;
537

538
    bool ret = archiveInterface()->extractFiles(m_entries, m_destinationDir, m_options);
539

540
    if (!archiveInterface()->waitForFinishedSignal()) {
541
        onFinished(ret);
542
    }
543
544
}

545
546
547
548
549
QString ExtractJob::destinationDirectory() const
{
    return m_destinationDir;
}

550
551
552
553
554
ExtractionOptions ExtractJob::extractionOptions() const
{
    return m_options;
}

555
TempExtractJob::TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
556
    : Job(interface)
557
    , m_entry(entry)
558
559
    , m_passwordProtectedHint(passwordProtectedHint)
{
560
    m_tmpExtractDir = new QTemporaryDir();
561
562
563
564
}

QString TempExtractJob::validatedFilePath() const
{
565
    QString path = extractionDir() + QLatin1Char('/') + m_entry->fullPath();
566
567
568
569
570
571
572
573
574
575
576
577
578
579

    // Make sure a maliciously crafted archive with parent folders named ".." do
    // not cause the previewed file path to be located outside the temporary
    // directory, resulting in a directory traversal issue.
    path.remove(QStringLiteral("../"));

    return path;
}

ExtractionOptions TempExtractJob::extractionOptions() const
{
    ExtractionOptions options;

    if (m_passwordProtectedHint) {
580
        options.setEncryptedArchiveHint(true);
581
582
583
584
585
    }

    return options;
}

586
587
588
589
590
QTemporaryDir *TempExtractJob::tempDir() const
{
    return m_tmpExtractDir;
}

591
592
void TempExtractJob::doWork()
{
593
    // pass 1 to i18np on purpose so this translation may properly be reused.
Alexander Lohnau's avatar
Alexander Lohnau committed
594
    Q_EMIT description(this, i18np("Extracting one file", "Extracting %1 files", 1));
595
596
597

    connectToArchiveInterfaceSignals();

598
    qCDebug(ARK) << "Extracting:" << m_entry;
599

600
    bool ret = archiveInterface()->extractFiles({m_entry}, extractionDir(), extractionOptions());
601
602
603
604
605
606

    if (!archiveInterface()->waitForFinishedSignal()) {
        onFinished(ret);
    }
}

607
QString TempExtractJob::extractionDir() const
608
{
609
    return m_tmpExtractDir->path();
610
611
}

612
613
PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
    : TempExtractJob(entry, passwordProtectedHint, interface)
614
{
615
    qCDebug(ARK) << "Created job instance";
616
617
}

618
619
OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
    : TempExtractJob(entry, passwordProtectedHint, interface)
620
{
621
    qCDebug(ARK) << "Created job instance";
622
623
}

624
625
OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
    : OpenJob(entry, passwordProtectedHint, interface)
626
{
627
    qCDebug(ARK) << "Created job instance";
628
629
}

630
AddJob::AddJob(const QVector<Archive::Entry*> &entries, const Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
631
    : Job(interface)
632
    , m_entries(entries)
633
    , m_destination(destination)
634
    , m_options(options)
635
{
636
    qCDebug(ARK) << "Created job instance";
637
638
639
640
}

void AddJob::doWork()
{
641
    // Set current dir.
642
    const QString globalWorkDir = m_options.globalWorkDir();
643
644
645
646
647
648
649
    const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir);
    if (!globalWorkDir.isEmpty()) {
        qCDebug(ARK) << "GlobalWorkDir is set, changing dir to " << globalWorkDir;
        m_oldWorkingDir = QDir::currentPath();
        QDir::setCurrent(globalWorkDir);
    }

650
    // Count total number of entries to be added.
651
    uint totalCount = 0;
652
653
    QElapsedTimer timer;
    timer.start();
654
    for (const Archive::Entry* entry : qAsConst(m_entries)) {
655
        totalCount++;
656
        if (QFileInfo(entry->fullPath()).isDir()) {
657
            QDirIterator it(entry->fullPath(), QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
658
659
660
661
662
663
664
            while (it.hasNext()) {
                it.next();
                totalCount++;
            }
        }
    }

665
    qCDebug(ARK) << "Going to add" << totalCount << "entries, counted in" << timer.elapsed() << "ms";
666

Elvis Angelaccio's avatar
Elvis Angelaccio committed
667
    const QString desc = i18np("Compressing a file", "Compressing %1 files", totalCount);
Alexander Lohnau's avatar
Alexander Lohnau committed
668
    Q_EMIT description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename()));
669
670
671
672
673
674

    ReadWriteArchiveInterface *m_writeInterface =
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());

    Q_ASSERT(m_writeInterface);

675
    // The file paths must be relative to GlobalWorkDir.
676
    for (Archive::Entry *entry : qAsConst(m_entries)) {
677
678
        // #191821: workDir must be used instead of QDir::current()
        //          so that symlinks aren't resolved automatically
679
        const QString &fullPath = entry->fullPath();
680
        QString relativePath = workDir.relativeFilePath(fullPath);
681

682
        if (fullPath.endsWith(QLatin1Char('/'))) {
683
684
685
            relativePath += QLatin1Char('/');
        }

686
        entry->setFullPath(relativePath);
687
688
    }

689
    connectToArchiveInterfaceSignals();
690
    bool ret = m_writeInterface->addFiles(m_entries, m_destination, m_options, totalCount);
691

692
    if (!archiveInterface()->waitForFinishedSignal()) {
693
        onFinished(ret);
694
    }
695
696
}

697
698
699
700
701
702
703
704
705
void AddJob::onFinished(bool result)
{
    if (!m_oldWorkingDir.isEmpty()) {
        QDir::setCurrent(m_oldWorkingDir);
    }

    Job::onFinished(result);
}

706
MoveJob::MoveJob(const QVector<Archive::Entry*> &entries, Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface)
707
708
709
710
711
712
    : Job(interface)
    , m_finishedSignalsCount(0)
    , m_entries(entries)
    , m_destination(destination)
    , m_options(options)
{
713
    qCDebug(ARK) << "Created job instance";
714
715
716
717
}

void MoveJob::doWork()
{
718
    qCDebug(ARK) << "Going to move" << m_entries.count() << "file(s)";
719

720
    QString desc = i18np("Moving a file", "Moving %1 files", m_entries.count());
Alexander Lohnau's avatar
Alexander Lohnau committed
721
    Q_EMIT description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename()));
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743

    ReadWriteArchiveInterface *m_writeInterface =
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());

    Q_ASSERT(m_writeInterface);

    connectToArchiveInterfaceSignals();
    bool ret = m_writeInterface->moveFiles(m_entries, m_destination, m_options);

    if (!archiveInterface()->waitForFinishedSignal()) {
        onFinished(ret);
    }
}

void MoveJob::onFinished(bool result)
{
    m_finishedSignalsCount++;
    if (m_finishedSignalsCount == archiveInterface()->moveRequiredSignals()) {
        Job::onFinished(result);
    }
}

744
CopyJob::CopyJob(const QVector<Archive::Entry*> &entries, Archive::Entry *destination, const CompressionOptions &options, ReadWriteArchiveInterface *interface)
745
746
747
748
749
750
    : Job(interface)
    , m_finishedSignalsCount(0)
    , m_entries(entries)
    , m_destination(destination)
    , m_options(options)
{
751
    qCDebug(ARK) << "Created job instance";
752
753
754
755
}

void CopyJob::doWork()
{
756
    qCDebug(ARK) << "Going to copy" << m_entries.count() << "file(s)";
757

758
    QString desc = i18np("Copying a file", "Copying %1 files", m_entries.count());
Alexander Lohnau's avatar
Alexander Lohnau committed
759
    Q_EMIT description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename()));
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781

    ReadWriteArchiveInterface *m_writeInterface =
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());

    Q_ASSERT(m_writeInterface);

    connectToArchiveInterfaceSignals();
    bool ret = m_writeInterface->copyFiles(m_entries, m_destination, m_options);

    if (!archiveInterface()->waitForFinishedSignal()) {
        onFinished(ret);
    }
}

void CopyJob::onFinished(bool result)
{
    m_finishedSignalsCount++;
    if (m_finishedSignalsCount == archiveInterface()->copyRequiredSignals()) {
        Job::onFinished(result);
    }
}

782
DeleteJob::DeleteJob(const QVector<Archive::Entry*> &entries, ReadWriteArchiveInterface *interface)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
783
    : Job(interface)
784
    , m_entries(entries)
785
786
787
788
789
{
}

void DeleteJob::doWork()
{
790
    QString desc = i18np("Deleting a file from the archive", "Deleting %1 files", m_entries.count());
Alexander Lohnau's avatar
Alexander Lohnau committed
791
    Q_EMIT description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename()));
792
793

    ReadWriteArchiveInterface *m_writeInterface =
794
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());
795
796
797

    Q_ASSERT(m_writeInterface);

798
    connectToArchiveInterfaceSignals();
799
    bool ret = m_writeInterface->deleteFiles(m_entries);
800

801
    if (!archiveInterface()->waitForFinishedSignal()) {
802
        onFinished(ret);
803
    }
804
}
805

Elvis Angelaccio's avatar
Elvis Angelaccio committed
806
807
CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface)
    : Job(interface)
808
809
810
811
812
813
    , m_comment(comment)
{
}

void CommentJob::doWork()
{
Alexander Lohnau's avatar
Alexander Lohnau committed
814
    Q_EMIT description(this, i18n("Adding comment"));
815
816
817
818
819
820
821
822
823
824
825
826
827
828

    ReadWriteArchiveInterface *m_writeInterface =
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());

    Q_ASSERT(m_writeInterface);

    connectToArchiveInterfaceSignals();
    bool ret = m_writeInterface->addComment(m_comment);

    if (!archiveInterface()->waitForFinishedSignal()) {
        onFinished(ret);
    }
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
829
830
TestJob::TestJob(ReadOnlyArchiveInterface *interface)
    : Job(interface)
Ragnar Thomsen's avatar
Ragnar Thomsen committed
831
832
833
834
835
836
{
    m_testSuccess = false;
}

void TestJob::doWork()
{
837
    qCDebug(ARK) << "Job started";
Ragnar Thomsen's avatar
Ragnar Thomsen committed
838

Alexander Lohnau's avatar
Alexander Lohnau committed
839
    Q_EMIT description(this, i18n("Testing archive"), qMakePair(i18n("Archive"), archiveInterface()->filename()));
840

Ragnar Thomsen's avatar
Ragnar Thomsen committed
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
    connectToArchiveInterfaceSignals();
    connect(archiveInterface(), &ReadOnlyArchiveInterface::testSuccess, this, &TestJob::onTestSuccess);

    bool ret = archiveInterface()->testArchive();

    if (!archiveInterface()->waitForFinishedSignal()) {
        onFinished(ret);
    }
}

void TestJob::onTestSuccess()
{
    m_testSuccess = true;
}

bool TestJob::testSucceeded()
{
    return m_testSuccess;
}

861
} // namespace Kerfuffle
862

Elvis Angelaccio's avatar
Elvis Angelaccio committed
863
#include "jobs.moc"