File indexing completed on 2024-05-05 05:00:04

0001 /*
0002     SPDX-FileCopyrightText: 2001 Andreas Schlapbach <schlpbch@iam.unibe.ch>
0003     SPDX-FileCopyrightText: 2003 Antonio Larrosa <larrosa@kde.org>
0004     SPDX-FileCopyrightText: 2008 Matthias Grimrath <maps4711@gmx.de>
0005     SPDX-FileCopyrightText: 2020 Jonathan Marten <jjm@keelhaul.me.uk>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "archivedialog.h"
0011 
0012 #include <QLayout>
0013 #include <QFormLayout>
0014 #include <QComboBox>
0015 #include <QCheckBox>
0016 #include <QMimeDatabase>
0017 #include <QMimeType>
0018 #include <QDialogButtonBox>
0019 #include <QTemporaryDir>
0020 #include <QTemporaryFile>
0021 #include <QStandardPaths>
0022 #include <QGroupBox>
0023 #include <QDesktopServices>
0024 #include <QGuiApplication>
0025 #include <QTimer>
0026 #include <QRegularExpression>
0027 
0028 #include <klocalizedstring.h>
0029 #include <kurlrequester.h>
0030 #include <kmessagebox.h>
0031 #include <ktar.h>
0032 #include <kzip.h>
0033 #include <krecentdirs.h>
0034 #include <ksharedconfig.h>
0035 #include <kconfiggroup.h>
0036 #include <kstandardguiitem.h>
0037 #include <kprotocolmanager.h>
0038 #include <kconfiggroup.h>
0039 #include <kpluralhandlingspinbox.h>
0040 
0041 #include <kio/statjob.h>
0042 #include <kio/deletejob.h>
0043 #include <kio/copyjob.h>
0044 
0045 #include "webarchiverdebug.h"
0046 #include "settings.h"
0047 
0048 
0049 ArchiveDialog::ArchiveDialog(const QUrl &url, QWidget *parent)
0050     : KMainWindow(parent)
0051 {
0052     setObjectName("ArchiveDialog");
0053 
0054     // Generate a default name for the web archive.
0055     // First try the file name of the URL, trimmed of any recognised suffix.
0056     QString archiveName = url.fileName();
0057     QMimeDatabase db;
0058     archiveName.chop(db.suffixForFileName(archiveName).length());
0059     if (archiveName.isEmpty())
0060     {
0061         //The implementation in KF5 constructed the archive name basing on QUrl::topLevelDomain()
0062         //Since that function doesn't exist anymore, and there's no easy way to replace it,
0063         //use a simpler algorithm: just replace each dot in the host with an underscore
0064         archiveName = url.host().replace(".", "_");             // host name from URL
0065 
0066     }
0067 
0068     // Find the last archive save location used
0069     QString dir = KRecentDirs::dir(":save");
0070     if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
0071     if (!dir.endsWith('/')) dir += '/';
0072     // Generate the base path and name for the archive file
0073     QString fileBase = dir+archiveName.simplified();
0074     qCDebug(WEBARCHIVERPLUGIN_LOG) << url << "->" << fileBase;
0075 
0076     m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Close, this);
0077 
0078     m_archiveButton = qobject_cast<QPushButton *>(m_buttonBox->button(QDialogButtonBox::Ok));
0079     Q_ASSERT(m_archiveButton!=nullptr);
0080     m_archiveButton->setDefault(true);
0081     m_archiveButton->setIcon(KStandardGuiItem::save().icon());
0082     m_archiveButton->setText(i18n("Create Archive"));
0083     connect(m_archiveButton, &QAbstractButton::clicked, this, &ArchiveDialog::slotCreateButtonClicked);
0084     m_buttonBox->addButton(m_archiveButton, QDialogButtonBox::ActionRole);
0085 
0086     m_cancelButton =  qobject_cast<QPushButton *>(m_buttonBox->button(QDialogButtonBox::Close));
0087     Q_ASSERT(m_cancelButton!=nullptr);
0088     connect(m_cancelButton, &QAbstractButton::clicked, this, &QWidget::close);
0089 
0090     QWidget *w = new QWidget(this);         // main widget
0091     QVBoxLayout *vbl = new QVBoxLayout(w);      // main vertical layout
0092     KConfigSkeletonItem *ski;               // config for creating widgets
0093 
0094     m_guiWidget = new QWidget(this);            // the main GUI widget
0095     QFormLayout *fl = new QFormLayout(m_guiWidget); // layout for entry form
0096 
0097     m_pageUrlReq = new KUrlRequester(url, this);
0098     m_pageUrlReq->setToolTip(i18n("The URL of the page that is to be archived"));
0099     slotSourceUrlChanged(m_pageUrlReq->text());
0100     connect(m_pageUrlReq, &KUrlRequester::textChanged, this, &ArchiveDialog::slotSourceUrlChanged);
0101     fl->addRow(i18n("Source &URL:"), m_pageUrlReq);
0102 
0103     fl->addRow(QString(), new QWidget(this));
0104 
0105     ski = Settings::self()->archiveTypeItem();
0106     Q_ASSERT(ski!=nullptr);
0107     m_typeCombo = new QComboBox(this);
0108     m_typeCombo->setSizePolicy(QSizePolicy::Expanding, m_typeCombo->sizePolicy().verticalPolicy());
0109     m_typeCombo->setToolTip(ski->toolTip());
0110     m_typeCombo->addItem(QIcon::fromTheme("webarchiver"), i18n("Web archive (*.war)"), "application/x-webarchive");
0111     m_typeCombo->addItem(QIcon::fromTheme("application-x-archive"), i18n("Tar archive (*.tar)"), "application/x-tar");
0112     m_typeCombo->addItem(QIcon::fromTheme("application-zip"), i18n("Zip archive (*.zip)"), "application/zip");
0113     m_typeCombo->addItem(QIcon::fromTheme("folder"), i18n("Directory"), "inode/directory");
0114     connect(m_typeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ArchiveDialog::slotArchiveTypeChanged);
0115     fl->addRow(ski->label(), m_typeCombo);
0116 
0117     m_saveUrlReq = new KUrlRequester(QUrl::fromLocalFile(fileBase), this);
0118     m_saveUrlReq->setToolTip(i18n("The file or directory where the archived page will be saved"));
0119     fl->addRow(i18n("&Save to:"), m_saveUrlReq);
0120 
0121     QGroupBox *grp = new QGroupBox(i18n("Options"), this);
0122     grp->setFlat(true);
0123     fl->addRow(grp);
0124 
0125     ski = Settings::self()->waitTimeItem();
0126     Q_ASSERT(ski!=nullptr);
0127     m_waitTimeSpinbox = new KPluralHandlingSpinBox(this);
0128     m_waitTimeSpinbox->setMinimumWidth(100);
0129     m_waitTimeSpinbox->setToolTip(ski->toolTip());
0130     m_waitTimeSpinbox->setRange(ski->minValue().toInt(), ski->maxValue().toInt());
0131     m_waitTimeSpinbox->setSuffix(ki18np(" second", " seconds"));
0132     m_waitTimeSpinbox->setSpecialValueText(i18n("None"));
0133     connect(m_waitTimeSpinbox, QOverload<int>::of(&QSpinBox::valueChanged), [=](int val) { m_randomWaitCheck->setEnabled(val>0); });
0134     fl->addRow(ski->label(), m_waitTimeSpinbox);
0135 
0136     fl->addRow(QString(), new QWidget(this));
0137 
0138     ski = Settings::self()->noProxyItem();
0139     Q_ASSERT(ski!=nullptr);
0140     m_noProxyCheck = new QCheckBox(ski->label(), this);
0141     m_noProxyCheck->setToolTip(ski->toolTip());
0142     fl->addRow(QString(), m_noProxyCheck);
0143 
0144     ski = Settings::self()->randomWaitItem();
0145     Q_ASSERT(ski!=nullptr);
0146     m_randomWaitCheck = new QCheckBox(ski->label(), this);
0147     m_randomWaitCheck->setToolTip(ski->toolTip());
0148     fl->addRow(QString(), m_randomWaitCheck);
0149 
0150     ski = Settings::self()->fixExtensionsItem();
0151     Q_ASSERT(ski!=nullptr);
0152     m_fixExtensionsCheck = new QCheckBox(ski->label(), this);
0153     m_fixExtensionsCheck->setToolTip(ski->toolTip());
0154     fl->addRow(QString(), m_fixExtensionsCheck);
0155 
0156     fl->addRow(QString(), new QWidget(this));
0157 
0158     ski = Settings::self()->runInTerminalItem();
0159     Q_ASSERT(ski!=nullptr);
0160     m_runInTerminalCheck = new QCheckBox(ski->label(), this);
0161     m_runInTerminalCheck->setToolTip(ski->toolTip());
0162     fl->addRow(QString(), m_runInTerminalCheck);
0163 
0164     ski = Settings::self()->closeWhenFinishedItem();
0165     Q_ASSERT(ski!=nullptr);
0166     m_closeWhenFinishedCheck = new QCheckBox(ski->label(), this);
0167     m_closeWhenFinishedCheck->setToolTip(ski->toolTip());
0168     fl->addRow(QString(), m_closeWhenFinishedCheck);
0169 
0170     vbl->addWidget(m_guiWidget);
0171     vbl->setStretchFactor(m_guiWidget, 1);
0172 
0173     m_messageWidget = new KMessageWidget(this);
0174     m_messageWidget->setWordWrap(true);
0175     m_messageWidget->hide();
0176     connect(m_messageWidget, &KMessageWidget::linkActivated, this, &ArchiveDialog::slotMessageLinkActivated);
0177     vbl->addWidget(m_messageWidget);
0178 
0179     vbl->addWidget(m_buttonBox);
0180     setCentralWidget(w);
0181 
0182     setAutoSaveSettings(objectName(), true);
0183     readSettings();
0184 
0185     m_tempDir = nullptr;
0186     m_tempFile = nullptr;
0187 
0188     // Check the current system proxy settings.  Being a command line tool,
0189     // wget(1) can only use proxy environment variables;  if KIO is set to
0190     // use these also then there is no problem.  Otherwise, warn the user
0191     // that the settings cannot be used.
0192     enum ProxySettings {NoProxy, EnvVarProxy, SpecialProxy};
0193     ProxySettings proxyType = NoProxy;
0194 #if QT_VERSION_MAJOR < 6
0195     const KProtocolManager::ProxyType proxyTypeFromProtocolManager = KProtocolManager::proxyType();
0196     if (proxyTypeFromProtocolManager == KProtocolManager::EnvVarProxy) {
0197         proxyType = EnvVarProxy;
0198     } else if (proxyTypeFromProtocolManager != KProtocolManager::NoProxy) {
0199         proxyType = SpecialProxy;
0200     }
0201 #else
0202     int proxyTypeAsInt = KSharedConfig::openConfig(QStringLiteral("kioslaverc"), KConfig::NoGlobals)->group("Proxy Settings").readEntry("ProxyType", 0);
0203     //According to kio-extras/kcms/ksaveioconfig.h, 0 means "No proxy" and 4 means "proxy from environment variable"
0204     if (proxyTypeAsInt == 4) {
0205         proxyType = EnvVarProxy;
0206     } else if (proxyTypeAsInt != 0) {
0207         proxyType = SpecialProxy;
0208     }
0209 #endif
0210     if (proxyType == NoProxy)       // no proxy configured.
0211     {                           // we cannot use one either
0212         m_noProxyCheck->setChecked(true);
0213         m_noProxyCheck->setEnabled(false);
0214     }
0215     else if (proxyType == SpecialProxy) // special KIO setting,
0216     {                           // but we cannot use it
0217         m_noProxyCheck->setChecked(true);
0218         m_noProxyCheck->setEnabled(false);
0219         showMessage(xi18nc("@info", "The web archive download cannot use the current proxy settings. No proxy will be used."),
0220                     KMessageWidget::Information);
0221     }
0222 
0223     slotArchiveTypeChanged(m_typeCombo->currentIndex());
0224 }
0225 
0226 
0227 ArchiveDialog::~ArchiveDialog()
0228 {
0229     cleanup();                      // process and temporary files
0230 }
0231 
0232 
0233 void ArchiveDialog::cleanup()
0234 {
0235     if (!m_archiveProcess.isNull()) m_archiveProcess->deleteLater();
0236 
0237     delete m_tempDir;
0238     m_tempDir = nullptr;
0239 
0240     if (m_tempFile!=nullptr)
0241     {
0242         m_tempFile->setAutoRemove(true);
0243         delete m_tempFile;
0244         m_tempFile = nullptr;
0245     }
0246 }
0247 
0248 
0249 void ArchiveDialog::slotSourceUrlChanged(const QString &text)
0250 {
0251     m_archiveButton->setEnabled(QUrl::fromUserInput(text).isValid());
0252 }
0253 
0254 
0255 void ArchiveDialog::slotArchiveTypeChanged(int idx)
0256 {
0257     const QString saveType = m_typeCombo->itemData(idx).toString();
0258     qCDebug(WEBARCHIVERPLUGIN_LOG) << saveType;
0259 
0260     QUrl url = m_saveUrlReq->url();
0261     url = url.adjusted(QUrl::StripTrailingSlash);
0262     QString fileName = url.fileName();
0263     url = url.adjusted(QUrl::RemoveFilename);
0264 
0265     QMimeDatabase db;
0266     fileName.chop(db.suffixForFileName(fileName).length());
0267     if (fileName.endsWith('.')) fileName.chop(1);
0268 
0269     if (saveType!="inode/directory")
0270     {
0271         const QMimeType mimeType = db.mimeTypeForName(saveType);
0272         fileName += '.';
0273         fileName += mimeType.preferredSuffix();
0274     }
0275 
0276     url.setPath(url.path()+fileName);
0277 
0278     if (saveType=="inode/directory") m_saveUrlReq->setMode(KFile::Directory);
0279     else m_saveUrlReq->setMode(KFile::File);
0280     m_saveUrlReq->setMimeTypeFilters(QStringList() << saveType);
0281     m_saveUrlReq->setUrl(url);
0282 }
0283 
0284 
0285 bool ArchiveDialog::queryClose()
0286 {
0287     // If the archive process is not running, the button is "Close"
0288     // and will just close the window.
0289     if (m_archiveProcess.isNull()) return (true);
0290 
0291     // Just signal the process here.  slotProcessFinished() will clean up
0292     // and ask whether to retain a partial download.
0293     m_archiveProcess->terminate();
0294     return (false);                 // don't close just yet
0295 }
0296 
0297 
0298 void ArchiveDialog::slotMessageLinkActivated(const QString &link)
0299 {
0300     QDesktopServices::openUrl(QUrl(link));
0301 }
0302 
0303 
0304 void ArchiveDialog::setGuiEnabled(bool on)
0305 {
0306     m_guiWidget->setEnabled(on);
0307     m_archiveButton->setEnabled(on);
0308     m_cancelButton->setText((on ? KStandardGuiItem::close() : KStandardGuiItem::cancel()).text());
0309 
0310     if (!on) QGuiApplication::setOverrideCursor(Qt::BusyCursor);
0311     else QGuiApplication::restoreOverrideCursor();
0312 }
0313 
0314 
0315 void ArchiveDialog::saveSettings()
0316 {
0317     Settings::setArchiveType(m_typeCombo->currentData().toString());
0318 
0319     Settings::setWaitTime(m_waitTimeSpinbox->value());
0320 
0321     if (m_noProxyCheck->isEnabled()) Settings::setNoProxy(m_noProxyCheck->isChecked());
0322     Settings::setRandomWait(m_randomWaitCheck->isChecked());
0323     Settings::setFixExtensions(m_fixExtensionsCheck->isChecked());
0324     Settings::setRunInTerminal(m_runInTerminalCheck->isChecked());
0325     Settings::setCloseWhenFinished(m_closeWhenFinishedCheck->isChecked());
0326 
0327     Settings::self()->save();
0328 }
0329 
0330 
0331 void ArchiveDialog::readSettings()
0332 {
0333     const int idx = m_typeCombo->findData(Settings::archiveType());
0334     if (idx!=-1) m_typeCombo->setCurrentIndex(idx);
0335 
0336     m_waitTimeSpinbox->setValue(Settings::waitTime());
0337 
0338     m_noProxyCheck->setChecked(Settings::noProxy());
0339     m_randomWaitCheck->setChecked(Settings::randomWait());
0340     m_fixExtensionsCheck->setChecked(Settings::fixExtensions());
0341     m_runInTerminalCheck->setChecked(Settings::runInTerminal());
0342     m_closeWhenFinishedCheck->setChecked(Settings::closeWhenFinished());
0343 
0344     m_randomWaitCheck->setEnabled(m_waitTimeSpinbox->value()>0);
0345 }
0346 
0347 
0348 void ArchiveDialog::slotCreateButtonClicked()
0349 {
0350     setGuiEnabled(false);               // while archiving is in progress
0351     showMessage("");                    // clear any existing message
0352 
0353     m_saveUrl = m_saveUrlReq->url();
0354     qCDebug(WEBARCHIVERPLUGIN_LOG) << m_saveUrl;
0355 
0356     if (!m_saveUrl.isValid())
0357     {
0358         showMessageAndCleanup(i18nc("@info", "The save location is not valid."), KMessageWidget::Error);
0359         return;
0360     }
0361 
0362     // Remember the archive save location used
0363     QUrl url = m_saveUrl.adjusted(QUrl::RemoveFilename);
0364     if (url.isValid()) KRecentDirs::add(":save", url.toString(QUrl::PreferLocalFile));
0365 
0366     // Also save the window size and the other GUI options.
0367     // From here on we can use Settings to access them.
0368     saveSettings();
0369 
0370     // Check that the wget(1) command is available before doing too much other work
0371     if (m_wgetProgram.isEmpty())
0372     {
0373         m_wgetProgram = QStandardPaths::findExecutable("wget");
0374         qCDebug(WEBARCHIVERPLUGIN_LOG) << "wget program" << m_wgetProgram;
0375         if (m_wgetProgram.isEmpty())
0376         {
0377             showMessageAndCleanup(xi18nc("@info",
0378                                          "Cannot find the wget(1) command,<nl/>see <link>%1</link>.",
0379                                          "https://www.gnu.org/software/wget"),
0380                                   KMessageWidget::Error);
0381             return;
0382         }
0383     }
0384 
0385     // Check whether the destination file or directory exists
0386 #if QT_VERSION_MAJOR < 6
0387     KIO::StatJob *statJob = KIO::statDetails(m_saveUrl, KIO::StatJob::DestinationSide, KIO::StatBasic);
0388 #else
0389     KIO::StatJob *statJob = KIO::stat(m_saveUrl, KIO::StatJob::DestinationSide, KIO::StatBasic);
0390 #endif
0391     connect(statJob, &KJob::result, this, &ArchiveDialog::slotCheckedDestination);
0392 }
0393 
0394 
0395 void ArchiveDialog::slotCheckedDestination(KJob *job)
0396 {
0397     KIO::StatJob *statJob = qobject_cast<KIO::StatJob *>(job);
0398     Q_ASSERT(statJob!=nullptr);
0399 
0400     const int err = job->error();
0401     if (err!=0 && err!=KIO::ERR_DOES_NOT_EXIST)
0402     {
0403         showMessageAndCleanup(xi18nc("@info",
0404                                      "Cannot verify destination<nl/><filename>%1</filename><nl/>%2",
0405                                      statJob->url().toDisplayString(), job->errorString()),
0406                               KMessageWidget::Error);
0407         return;
0408     }
0409 
0410     if (err==0) m_saveUrl = statJob->mostLocalUrl();    // update to most local form
0411     m_saveType = m_typeCombo->itemData(m_typeCombo->currentIndex()).toString();
0412     qCDebug(WEBARCHIVERPLUGIN_LOG) << m_saveUrl << "as" << m_saveType;
0413 
0414     if (err==0)                     // destination already exists
0415     {
0416         const bool isDir = statJob->statResult().isDir();
0417         const QString url = m_saveUrl.toDisplayString(QUrl::PreferLocalFile);
0418         QString message;
0419         if (m_saveType=="inode/directory")
0420         {
0421             if (!isDir) message = xi18nc("@info", "The archive directory<nl/><filename>%1</filename><nl/>already exists as a file.", url);
0422             else message = xi18nc("@info", "The archive directory<nl/><filename>%1</filename><nl/>already exists.", url);
0423         }
0424         else
0425         {
0426             if (isDir) message = xi18nc("@info", "The archive file<nl/><filename>%1</filename><nl/>already exists as a directory.", url);
0427             else message = xi18nc("@info", "The archive file <nl/><filename>%1</filename><nl/>already exists.", url);
0428         }
0429 
0430         int result = KMessageBox::warningContinueCancel(this, message,
0431                                                         i18n("Archive Already Exists"),
0432                                                         KStandardGuiItem::overwrite(),
0433                                                         KStandardGuiItem::cancel(),
0434                                                         QString(),
0435                                                         KMessageBox::Dangerous);
0436         if (result==KMessageBox::Cancel)
0437         {
0438             showMessageAndCleanup("");
0439             return;
0440         }
0441 
0442         KIO::DeleteJob *delJob = KIO::del(m_saveUrl);
0443         connect(delJob, &KJob::result, this, &ArchiveDialog::slotDeletedOldDestination);
0444         return;
0445     }
0446 
0447     slotDeletedOldDestination(nullptr);
0448 }
0449 
0450 
0451 void ArchiveDialog::slotDeletedOldDestination(KJob *job)
0452 {
0453     if (job!=nullptr)
0454     {
0455         KIO::DeleteJob *delJob = qobject_cast<KIO::DeleteJob *>(job);
0456         Q_ASSERT(delJob!=nullptr);
0457 
0458         if (job->error())
0459         {
0460             showMessageAndCleanup(xi18nc("@info",
0461                                          "Cannot delete original archive<nl/><filename>%1</filename><nl/>%2",
0462                                          m_saveUrl.toDisplayString(), job->errorString()),
0463                                   KMessageWidget::Error);
0464             return;
0465         }
0466     }
0467 
0468     startDownloadProcess();
0469 }
0470 
0471 
0472 void ArchiveDialog::startDownloadProcess()
0473 {
0474     m_tempDir = new QTemporaryDir;
0475     if (!m_tempDir->isValid())
0476     {
0477         showMessageAndCleanup(xi18nc("@info",
0478                                      "Cannot create a temporary directory<nl/><filename>%1</filename><nl/>",
0479                                      m_tempDir->path()),
0480                               KMessageWidget::Error);
0481         return;
0482     }
0483     qCDebug(WEBARCHIVERPLUGIN_LOG) << "temp dir" << m_tempDir->path();
0484 
0485     QProcess *proc = new QProcess(this);
0486     proc->setProcessChannelMode(QProcess::ForwardedChannels);
0487     proc->setStandardInputFile(QProcess::nullDevice());
0488     proc->setWorkingDirectory(m_tempDir->path());
0489     Q_ASSERT(!m_wgetProgram.isEmpty());         // should have found this earlier
0490     proc->setProgram(m_wgetProgram);
0491 
0492     QStringList args;                   // argument list for command
0493     args << "-p";                   // fetch page and all requirements
0494     args << "-k";                   // convert to relative links
0495     args << "-nH";                  // do not create host directories
0496     // This option is incompatible with '-k'
0497     //args << "-nc";                    // no clobber of existing files
0498     args << "-H";                   // fetch from foreign hosts
0499     args << "-nv";                  // not quite so verbose
0500     args << "--progress=dot:default";           // progress indication
0501     args << "-R" << "robots.txt";           // ignore this file
0502 
0503     if (Settings::fixExtensions())          // want standard archive format
0504     {
0505         args << "-nd";                  // no subdirectory structure
0506         args << "-E";                   // fix up file extensions
0507     }
0508 
0509     const int waitTime = Settings::waitTime();
0510     if (waitTime>0)                 // wait time requested?
0511     {
0512         args << "-w" << QString::number(waitTime);  // wait time between requests
0513         if (Settings::randomWait())
0514         {
0515             args << "--random-wait";            // randomise wait time
0516         }
0517     }
0518 
0519     if (Settings::noProxy())                // no proxy requested?
0520     {
0521         args << "--no-proxy";               // do not use proxy
0522     }
0523 
0524     args << m_pageUrlReq->url().toEncoded();        // finally the page URL
0525 
0526     qCDebug(WEBARCHIVERPLUGIN_LOG) << "wget args" << args;
0527     if (Settings::runInTerminal())          // running in a terminal?
0528     {
0529         args.prepend(proc->program());          // prepend existing "wget"
0530         args.prepend("-e");             // terminal command to execute
0531         args.prepend("--hold");             // then terminal options
0532 
0533         // from kservice/src/kdeinit/ktoolinvocation_x11.cpp
0534         KConfigGroup generalGroup(KSharedConfig::openConfig(), "General");
0535         const QString term = generalGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole"));
0536         proc->setProgram(term);             // set terminal as program
0537 
0538         qCDebug(WEBARCHIVERPLUGIN_LOG) << "terminal" << term << "args" << args;
0539     }
0540     proc->setArguments(args);
0541 
0542     connect(proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
0543             this, &ArchiveDialog::slotProcessFinished);
0544 
0545     m_archiveProcess = proc;                // note for cleanup later
0546     proc->start();                  // start the archiver process
0547     if (!proc->waitForStarted(2000))
0548     {
0549         showMessageAndCleanup(xi18nc("@info",
0550                                      "Cannot start the archiver process <filename>%1</filename>",
0551                                      m_wgetProgram),
0552                               KMessageWidget::Error);
0553         return;
0554     }
0555 }
0556 
0557 
0558 void ArchiveDialog::slotProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
0559 {
0560     qCDebug(WEBARCHIVERPLUGIN_LOG) << "code" << exitCode << "status" << exitStatus;
0561 
0562     QString message;
0563     if (exitStatus==QProcess::CrashExit)
0564     {
0565         // See if we terminated the process ourselves via queryClose()
0566         if (exitCode==SIGTERM) message = xi18nc("@info", "The download process was interrupted.");
0567         else message = xi18nc("@info", "<para>The download process <filename>%1</filename> failed with signal %2.</para>",
0568                               m_archiveProcess->program(), QString::number(exitCode));
0569     }
0570     else if (exitCode!=0)
0571     {
0572         message = xi18nc("@info", "<para>The download process <filename>%1</filename> failed with status %2.</para>",
0573                          m_archiveProcess->program(), QString::number(exitCode));
0574         if (exitCode==8)
0575         {
0576             message += xi18nc("@info", "<para>This may simply indicate a 404 error for some of the page elements.</para>");
0577         }
0578     }
0579 
0580     if (!message.isEmpty())
0581     {
0582         message += xi18nc("@info", "<para>Retain the partial web archive?</para>");
0583         if (KMessageBox::questionTwoActions(this, message, i18n("Retain Archive?"),
0584                                        KGuiItem(i18n("Retain"), KStandardGuiItem::save().icon()),
0585                                        KStandardGuiItem::discard())==KMessageBox::SecondaryAction)
0586         {
0587             showMessageAndCleanup(xi18nc("@info",
0588                                          "Creating the web archive failed."),
0589                                   KMessageWidget::Warning);
0590             return;
0591         }
0592     }
0593 
0594     finishArchive();
0595 }
0596 
0597 
0598 void ArchiveDialog::finishArchive()
0599 {
0600     QDir tempDir(m_tempDir->path());            // where the download now is
0601 
0602     if (Settings::fixExtensions())
0603     {
0604         // Look at the names at the top level of the temporary download directory,
0605         // and see if there is a single HTML file there.  If there is, then depending
0606         // on whether the original URL ended with a slash (which we cannot check or
0607         // correct because we cannot know whether it should or not), wget(1) may
0608         // save the HTML page as "lastcomponent.html" instead of "index.html".
0609         //
0610         // If this is the case, then rename the file in question to "index.html".
0611         // This is so that a web archive will open in Konqueror showing the HTML
0612         // page as intended.
0613         //
0614         // If saving as a directory or as another type of archive file, then
0615         // this does not apply.  However, do the rename anyway so that the
0616         // file naming is consistent regardless of what is being saved.
0617 
0618         QRegularExpression rx(QStringLiteral("(?!^index)\\.html?$"), QRegularExpression::CaseInsensitiveOption);        // a negative lookahead assertion!
0619         QString indexHtml;              // the index file found
0620 
0621         // This listing can simply use QDir::entryList() because only the
0622         // file names are needed.
0623         const QStringList entries = tempDir.entryList(QDir::Dirs|QDir::Files|QDir::QDir::NoDotAndDotDot);
0624         qCDebug(WEBARCHIVERPLUGIN_LOG) << "found" << entries.count() << "entries";
0625 
0626         for (const QString &name : entries)     // first pass, check file names
0627         {
0628             if (name.contains(rx))          // matches "anythingelse.html"
0629             {                       // but not "index.html"
0630                 if (!indexHtml.isEmpty())       // already have found something
0631                 {
0632                     qCDebug(WEBARCHIVERPLUGIN_LOG) << "multiple HTML files at top level";
0633                     indexHtml.clear();          // forget trying to rename
0634                     break;
0635                 }
0636 
0637                 qCDebug(WEBARCHIVERPLUGIN_LOG) << "identified index file" << name;
0638                 indexHtml = name;
0639             }
0640         }
0641 
0642         if (!indexHtml.isEmpty())           // have identified index file
0643         {
0644             tempDir.rename(indexHtml, "index.html");    // rename it to standard name
0645         }
0646     }
0647 
0648     QString sourcePath;                 // archive to be copied
0649 
0650     // The archived web page is now ready in the temporary directory.
0651     // If it is required to be saved as a file, then create a
0652     // temporary archive file in the same place.
0653     if (m_saveType!="inode/directory")          // saving as archive file
0654     {
0655         QMimeDatabase db;
0656         const QString ext = db.mimeTypeForName(m_saveType).preferredSuffix();
0657         m_tempFile = new QTemporaryFile(QDir::tempPath()+'/'+
0658                                         QCoreApplication::applicationName()+
0659                                         "-XXXXXX."+ext);
0660         m_tempFile->setAutoRemove(false);
0661         m_tempFile->open();
0662         QString tempArchive = m_tempFile->fileName();
0663         qCDebug(WEBARCHIVERPLUGIN_LOG) << "temp archive" << tempArchive;
0664         m_tempFile->close();                // only want the name
0665 
0666         KArchive *archive;
0667         if (m_saveType=="application/zip")
0668         {
0669             archive = new KZip(tempArchive);
0670         }
0671         else
0672         {
0673             if (m_saveType=="application/x-webarchive")
0674             {
0675                 // A web archive is a gzip-compressed tar file
0676                 archive = new KTar(tempArchive, "application/x-gzip");
0677             }
0678             else archive = new KTar(tempArchive);
0679         }
0680         archive->open(QIODevice::WriteOnly);
0681 
0682         // Read each entry in the temporary directory and add it to the archive.
0683         // Cannnot simply use addLocalDirectory(m_tempDir->path()) here, because
0684         // that would add an extra directory level within the archive having the
0685         // random name of the temporary directory.
0686         //
0687         // This listing needs to use QDir::entryInfoList() so that adding to the
0688         // archive can distinguish between files and directories.  The list needs
0689         // to be refreshed because the page HTML file could have been renamed above.
0690         const QFileInfoList entries = tempDir.entryInfoList(QDir::Dirs|QDir::Files|QDir::QDir::NoDotAndDotDot);
0691         qCDebug(WEBARCHIVERPLUGIN_LOG) << "adding" << entries.count() << "entries";
0692 
0693         for (const QFileInfo &fi : entries)     // second pass, write out entries
0694         {
0695             if (fi.isFile())
0696             {
0697                 qCDebug(WEBARCHIVERPLUGIN_LOG) << "  adding file" << fi.absoluteFilePath();
0698                 archive->addLocalFile(fi.absoluteFilePath(), fi.fileName());
0699             }
0700             else if (fi.isDir())
0701             {
0702                 qCDebug(WEBARCHIVERPLUGIN_LOG) << "  adding dir" << fi.absoluteFilePath();
0703                 archive->addLocalDirectory(fi.absoluteFilePath(), fi.fileName());
0704             }
0705             else qCDebug(WEBARCHIVERPLUGIN_LOG) << "unrecognised entry type for" << fi.fileName();
0706         }
0707 
0708         archive->close();               // finished with archive file
0709         sourcePath = tempArchive;           // source path to copy
0710     }
0711     else                        // saving as a directory
0712     {
0713         sourcePath = tempDir.absolutePath();        // source path to copy
0714     }
0715 
0716     // Finally copy the temporary file or directory to the requested save location
0717     KIO::CopyJob *copyJob = KIO::copyAs(QUrl::fromLocalFile(sourcePath), m_saveUrl);
0718     connect(copyJob, &KJob::result, this, &ArchiveDialog::slotCopiedArchive);
0719 }
0720 
0721 
0722 void ArchiveDialog::slotCopiedArchive(KJob *job)
0723 {
0724     KIO::CopyJob *copyJob = qobject_cast<KIO::CopyJob *>(job);
0725     Q_ASSERT(copyJob!=nullptr);
0726     const QUrl destUrl = copyJob->destUrl();
0727 
0728     if (job->error())
0729     {
0730         showMessageAndCleanup(xi18nc("@info",
0731                                      "Cannot copy archive to<nl/><filename>%1</filename><nl/>%2",
0732                                      destUrl.toDisplayString(), job->errorString()),
0733                               KMessageWidget::Error);
0734         return;
0735     }
0736 
0737     // Explicitly set permissions on the saved archive file or directory,
0738     // to honour the user's umask(2) setting.  This is needed because
0739     // both QTemporaryFile and QTemporaryDir create them with restrictive
0740     // permissions (as indeed they should) by default.  The files within
0741     // the temporary directory will have been written by wget(1) with
0742     // standard creation permissions, so it does not need to be done
0743     // recursively.
0744     const mode_t perms = (m_saveType=="inode/directory") ? 0777 : 0666;
0745     const mode_t mask = umask(0); umask(mask);
0746 
0747     KIO::SimpleJob *chmodJob = KIO::chmod(destUrl, (perms & ~mask));
0748     connect(chmodJob, &KJob::result, this, &ArchiveDialog::slotFinishedArchive);
0749 }
0750 
0751 
0752 void ArchiveDialog::slotFinishedArchive(KJob *job)
0753 {
0754     KIO::SimpleJob *chmodJob = qobject_cast<KIO::SimpleJob *>(job);
0755     Q_ASSERT(chmodJob!=nullptr);
0756     const QUrl destUrl = chmodJob->url();
0757 
0758     if (job->error())
0759     {
0760         showMessageAndCleanup(xi18nc("@info",
0761                                      "Cannot set permissions on<nl/><filename>%1</filename><nl/>%2",
0762                                      destUrl.toDisplayString(), job->errorString()),
0763                               KMessageWidget::Warning);
0764         return;                     // do not close even if requested
0765     }
0766     else
0767     {
0768         showMessageAndCleanup(xi18nc("@info",
0769                                      "Web archive saved as<nl/><filename><link>%1</link></filename>",
0770                                      destUrl.toDisplayString()),
0771                               KMessageWidget::Positive);
0772     }
0773 
0774     // Now the archiving task is finished.
0775     if (Settings::closeWhenFinished())
0776     {
0777         // Let the user briefly see the completion message.
0778         QTimer::singleShot(1000, qApp, &QCoreApplication::quit);
0779     }
0780 }
0781 
0782 
0783 void ArchiveDialog::showMessageAndCleanup(const QString &text, KMessageWidget::MessageType type)
0784 {
0785     showMessage(text, type);
0786     cleanup();
0787     setGuiEnabled(true);
0788 }
0789 
0790 
0791 void ArchiveDialog::showMessage(const QString &text, KMessageWidget::MessageType type)
0792 {
0793     if (text.isEmpty())                 // remove existing message
0794     {
0795         m_messageWidget->hide();
0796         return;
0797     }
0798 
0799     QString iconName;
0800     switch (type)
0801     {
0802 case KMessageWidget::Positive:      iconName = "dialog-ok";         break;
0803 case KMessageWidget::Information:   iconName = "dialog-information";    break;
0804 default:
0805 case KMessageWidget::Warning:       iconName = "dialog-warning";        break;
0806 case KMessageWidget::Error:     iconName = "dialog-error";      break;
0807     }
0808 
0809     m_messageWidget->setCloseButtonVisible(type!=KMessageWidget::Positive && type!=KMessageWidget::Information);
0810     m_messageWidget->setIcon(QIcon::fromTheme(iconName));
0811     m_messageWidget->setMessageType(type);
0812     m_messageWidget->setText(text);
0813     m_messageWidget->show();
0814 }