1 /*
2     SPDX-FileCopyrightText: 2001 Andreas Schlapbach <schlpbch@iam.unibe.ch>
3     SPDX-FileCopyrightText: 2003 Antonio Larrosa <larrosa@kde.org>
4     SPDX-FileCopyrightText: 2008 Matthias Grimrath <maps4711@gmx.de>
5     SPDX-FileCopyrightText: 2020 Jonathan Marten <jjm@keelhaul.me.uk>
6 
7     SPDX-License-Identifier: GPL-2.0-or-later
8 */
9 
10 #include "archivedialog.h"
11 
12 #include <qlayout.h>
13 #include <qformlayout.h>
14 #include <qcombobox.h>
15 #include <qcheckbox.h>
16 #include <qmimedatabase.h>
17 #include <qmimetype.h>
18 #include <qdialogbuttonbox.h>
19 #include <qtemporarydir.h>
20 #include <qtemporaryfile.h>
21 #include <qstandardpaths.h>
22 #include <qgroupbox.h>
23 #include <qdesktopservices.h>
24 #include <qguiapplication.h>
25 #include <qtimer.h>
26 
27 #include <klocalizedstring.h>
28 #include <kurlrequester.h>
29 #include <kmessagebox.h>
30 #include <ktar.h>
31 #include <kzip.h>
32 #include <krecentdirs.h>
33 #include <ksharedconfig.h>
34 #include <kconfiggroup.h>
35 #include <kstandardguiitem.h>
36 #include <kprotocolmanager.h>
37 #include <kconfiggroup.h>
38 #include <kpluralhandlingspinbox.h>
39 
40 #include <kio/statjob.h>
41 #include <kio/deletejob.h>
42 #include <kio/copyjob.h>
43 
44 #include "webarchiverdebug.h"
45 #include "settings.h"
46 
47 
ArchiveDialog(const QUrl & url,QWidget * parent)48 ArchiveDialog::ArchiveDialog(const QUrl &url, QWidget *parent)
49     : KMainWindow(parent)
50 {
51     setObjectName("ArchiveDialog");
52 
53     // Generate a default name for the web archive.
54     // First try the file name of the URL, trimmed of any recognised suffix.
55     QString archiveName = url.fileName();
56     QMimeDatabase db;
57     archiveName.chop(db.suffixForFileName(archiveName).length());
58     if (archiveName.isEmpty())
59     {
60         // If there was no file name, then generate a name from the host name.
61         // Remove the top level domain suffix, then take the last component.
62         // For example, QUrl::topLevelDomain("http://www.kde.org") gives ".org"
63         // which leaves "www.kde", the last component is "kde" which is used
64         // as the archive base name.
65 
66         QString host = url.host();				// host name from URL
67         const QString tld = url.topLevelDomain(QUrl::EncodeUnicode);
68         if (!tld.isEmpty()) host.chop(tld.length());		// remove TLD suffix
69 
70         const int idx = host.lastIndexOf(QLatin1Char('.'));	// get last remaining component
71         host = host.mid(idx+1);					// works even if no '.'
72         archiveName = host;
73     }
74 
75     // Unable to generate a default archive name
76     if (archiveName.isEmpty()) archiveName = i18n("Untitled");
77 
78     // Find the last archive save location used
79     QString dir = KRecentDirs::dir(":save");
80     if (dir.isEmpty()) dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
81     if (!dir.endsWith('/')) dir += '/';
82     // Generate the base path and name for the archive file
83     QString fileBase = dir+archiveName.simplified();
84     qCDebug(WEBARCHIVERPLUGIN_LOG) << url << "->" << fileBase;
85 
86     m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Close, this);
87 
88     m_archiveButton = qobject_cast<QPushButton *>(m_buttonBox->button(QDialogButtonBox::Ok));
89     Q_ASSERT(m_archiveButton!=nullptr);
90     m_archiveButton->setDefault(true);
91     m_archiveButton->setIcon(KStandardGuiItem::save().icon());
92     m_archiveButton->setText(i18n("Create Archive"));
93     connect(m_archiveButton, &QAbstractButton::clicked, this, &ArchiveDialog::slotCreateButtonClicked);
94     m_buttonBox->addButton(m_archiveButton, QDialogButtonBox::ActionRole);
95 
96     m_cancelButton =  qobject_cast<QPushButton *>(m_buttonBox->button(QDialogButtonBox::Close));
97     Q_ASSERT(m_cancelButton!=nullptr);
98     connect(m_cancelButton, &QAbstractButton::clicked, this, &QWidget::close);
99 
100     QWidget *w = new QWidget(this);			// main widget
101     QVBoxLayout *vbl = new QVBoxLayout(w);		// main vertical layout
102     KConfigSkeletonItem *ski;				// config for creating widgets
103 
104     m_guiWidget = new QWidget(this);			// the main GUI widget
105     QFormLayout *fl = new QFormLayout(m_guiWidget);	// layout for entry form
106 
107     m_pageUrlReq = new KUrlRequester(url, this);
108     m_pageUrlReq->setToolTip(i18n("The URL of the page that is to be archived"));
109     slotSourceUrlChanged(m_pageUrlReq->text());
110     connect(m_pageUrlReq, &KUrlRequester::textChanged, this, &ArchiveDialog::slotSourceUrlChanged);
111     fl->addRow(i18n("Source &URL:"), m_pageUrlReq);
112 
113     fl->addRow(QString(), new QWidget(this));
114 
115     ski = Settings::self()->archiveTypeItem();
116     Q_ASSERT(ski!=nullptr);
117     m_typeCombo = new QComboBox(this);
118     m_typeCombo->setSizePolicy(QSizePolicy::Expanding, m_typeCombo->sizePolicy().verticalPolicy());
119     m_typeCombo->setToolTip(ski->toolTip());
120     m_typeCombo->addItem(QIcon::fromTheme("webarchiver"), i18n("Web archive (*.war)"), "application/x-webarchive");
121     m_typeCombo->addItem(QIcon::fromTheme("application-x-archive"), i18n("Tar archive (*.tar)"), "application/x-tar");
122     m_typeCombo->addItem(QIcon::fromTheme("application-zip"), i18n("Zip archive (*.zip)"), "application/zip");
123     m_typeCombo->addItem(QIcon::fromTheme("folder"), i18n("Directory"), "inode/directory");
124     connect(m_typeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ArchiveDialog::slotArchiveTypeChanged);
125     fl->addRow(ski->label(), m_typeCombo);
126 
127     m_saveUrlReq = new KUrlRequester(QUrl::fromLocalFile(fileBase), this);
128     m_saveUrlReq->setToolTip(i18n("The file or directory where the archived page will be saved"));
129     fl->addRow(i18n("&Save to:"), m_saveUrlReq);
130 
131     QGroupBox *grp = new QGroupBox(i18n("Options"), this);
132     grp->setFlat(true);
133     fl->addRow(grp);
134 
135     ski = Settings::self()->waitTimeItem();
136     Q_ASSERT(ski!=nullptr);
137     m_waitTimeSpinbox = new KPluralHandlingSpinBox(this);
138     m_waitTimeSpinbox->setMinimumWidth(100);
139     m_waitTimeSpinbox->setToolTip(ski->toolTip());
140     m_waitTimeSpinbox->setRange(ski->minValue().toInt(), ski->maxValue().toInt());
141     m_waitTimeSpinbox->setSuffix(ki18np(" second", " seconds"));
142     m_waitTimeSpinbox->setSpecialValueText(i18n("None"));
143     connect(m_waitTimeSpinbox, QOverload<int>::of(&QSpinBox::valueChanged), [=](int val) { m_randomWaitCheck->setEnabled(val>0); });
144     fl->addRow(ski->label(), m_waitTimeSpinbox);
145 
146     fl->addRow(QString(), new QWidget(this));
147 
148     ski = Settings::self()->noProxyItem();
149     Q_ASSERT(ski!=nullptr);
150     m_noProxyCheck = new QCheckBox(ski->label(), this);
151     m_noProxyCheck->setToolTip(ski->toolTip());
152     fl->addRow(QString(), m_noProxyCheck);
153 
154     ski = Settings::self()->randomWaitItem();
155     Q_ASSERT(ski!=nullptr);
156     m_randomWaitCheck = new QCheckBox(ski->label(), this);
157     m_randomWaitCheck->setToolTip(ski->toolTip());
158     fl->addRow(QString(), m_randomWaitCheck);
159 
160     ski = Settings::self()->fixExtensionsItem();
161     Q_ASSERT(ski!=nullptr);
162     m_fixExtensionsCheck = new QCheckBox(ski->label(), this);
163     m_fixExtensionsCheck->setToolTip(ski->toolTip());
164     fl->addRow(QString(), m_fixExtensionsCheck);
165 
166     fl->addRow(QString(), new QWidget(this));
167 
168     ski = Settings::self()->runInTerminalItem();
169     Q_ASSERT(ski!=nullptr);
170     m_runInTerminalCheck = new QCheckBox(ski->label(), this);
171     m_runInTerminalCheck->setToolTip(ski->toolTip());
172     fl->addRow(QString(), m_runInTerminalCheck);
173 
174     ski = Settings::self()->closeWhenFinishedItem();
175     Q_ASSERT(ski!=nullptr);
176     m_closeWhenFinishedCheck = new QCheckBox(ski->label(), this);
177     m_closeWhenFinishedCheck->setToolTip(ski->toolTip());
178     fl->addRow(QString(), m_closeWhenFinishedCheck);
179 
180     vbl->addWidget(m_guiWidget);
181     vbl->setStretchFactor(m_guiWidget, 1);
182 
183     m_messageWidget = new KMessageWidget(this);
184     m_messageWidget->setWordWrap(true);
185     m_messageWidget->hide();
186     connect(m_messageWidget, &KMessageWidget::linkActivated, this, &ArchiveDialog::slotMessageLinkActivated);
187     vbl->addWidget(m_messageWidget);
188 
189     vbl->addWidget(m_buttonBox);
190     setCentralWidget(w);
191 
192     setAutoSaveSettings(objectName(), true);
193     readSettings();
194 
195     m_tempDir = nullptr;
196     m_tempFile = nullptr;
197 
198     // Check the current system proxy settings.  Being a command line tool,
199     // wget(1) can only use proxy environment variables;  if KIO is set to
200     // use these also then there is no problem.  Otherwise, warn the user
201     // that the settings cannot be used.
202     const KProtocolManager::ProxyType proxyType = KProtocolManager::proxyType();
203     if (proxyType==KProtocolManager::NoProxy)		// no proxy configured.
204     {							// we cannot use one either
205         m_noProxyCheck->setChecked(true);
206         m_noProxyCheck->setEnabled(false);
207     }
208     else if (proxyType!=KProtocolManager::EnvVarProxy)	// special KIO setting,
209     {							// but we cannot use it
210         m_noProxyCheck->setChecked(true);
211         m_noProxyCheck->setEnabled(false);
212         showMessage(xi18nc("@info", "The web archive download cannot use the current proxy settings. No proxy will be used."),
213                     KMessageWidget::Information);
214     }
215 
216     slotArchiveTypeChanged(m_typeCombo->currentIndex());
217 }
218 
219 
~ArchiveDialog()220 ArchiveDialog::~ArchiveDialog()
221 {
222     cleanup();						// process and temporary files
223 }
224 
225 
cleanup()226 void ArchiveDialog::cleanup()
227 {
228     if (!m_archiveProcess.isNull()) m_archiveProcess->deleteLater();
229 
230     delete m_tempDir;
231     m_tempDir = nullptr;
232 
233     if (m_tempFile!=nullptr)
234     {
235         m_tempFile->setAutoRemove(true);
236         delete m_tempFile;
237         m_tempFile = nullptr;
238     }
239 }
240 
241 
slotSourceUrlChanged(const QString & text)242 void ArchiveDialog::slotSourceUrlChanged(const QString &text)
243 {
244     m_archiveButton->setEnabled(QUrl::fromUserInput(text).isValid());
245 }
246 
247 
slotArchiveTypeChanged(int idx)248 void ArchiveDialog::slotArchiveTypeChanged(int idx)
249 {
250     const QString saveType = m_typeCombo->itemData(idx).toString();
251     qCDebug(WEBARCHIVERPLUGIN_LOG) << saveType;
252 
253     QUrl url = m_saveUrlReq->url();
254     url = url.adjusted(QUrl::StripTrailingSlash);
255     QString fileName = url.fileName();
256     url = url.adjusted(QUrl::RemoveFilename);
257 
258     QMimeDatabase db;
259     fileName.chop(db.suffixForFileName(fileName).length());
260     if (fileName.endsWith('.')) fileName.chop(1);
261 
262     if (saveType!="inode/directory")
263     {
264         const QMimeType mimeType = db.mimeTypeForName(saveType);
265         fileName += '.';
266         fileName += mimeType.preferredSuffix();
267     }
268 
269     url.setPath(url.path()+fileName);
270 
271     if (saveType=="inode/directory") m_saveUrlReq->setMode(KFile::Directory);
272     else m_saveUrlReq->setMode(KFile::File);
273     m_saveUrlReq->setMimeTypeFilters(QStringList() << saveType);
274     m_saveUrlReq->setUrl(url);
275 }
276 
277 
queryClose()278 bool ArchiveDialog::queryClose()
279 {
280     // If the archive process is not running, the button is "Close"
281     // and will just close the window.
282     if (m_archiveProcess.isNull()) return (true);
283 
284     // Just signal the process here.  slotProcessFinished() will clean up
285     // and ask whether to retain a partial download.
286     m_archiveProcess->terminate();
287     return (false);					// don't close just yet
288 }
289 
290 
slotMessageLinkActivated(const QString & link)291 void ArchiveDialog::slotMessageLinkActivated(const QString &link)
292 {
293     QDesktopServices::openUrl(QUrl(link));
294 }
295 
296 
setGuiEnabled(bool on)297 void ArchiveDialog::setGuiEnabled(bool on)
298 {
299     m_guiWidget->setEnabled(on);
300     m_archiveButton->setEnabled(on);
301     m_cancelButton->setText((on ? KStandardGuiItem::close() : KStandardGuiItem::cancel()).text());
302 
303     if (!on) QGuiApplication::setOverrideCursor(Qt::BusyCursor);
304     else QGuiApplication::restoreOverrideCursor();
305 }
306 
307 
saveSettings()308 void ArchiveDialog::saveSettings()
309 {
310     Settings::setArchiveType(m_typeCombo->currentData().toString());
311 
312     Settings::setWaitTime(m_waitTimeSpinbox->value());
313 
314     if (m_noProxyCheck->isEnabled()) Settings::setNoProxy(m_noProxyCheck->isChecked());
315     Settings::setRandomWait(m_randomWaitCheck->isChecked());
316     Settings::setFixExtensions(m_fixExtensionsCheck->isChecked());
317     Settings::setRunInTerminal(m_runInTerminalCheck->isChecked());
318     Settings::setCloseWhenFinished(m_closeWhenFinishedCheck->isChecked());
319 
320     Settings::self()->save();
321 }
322 
323 
readSettings()324 void ArchiveDialog::readSettings()
325 {
326     const int idx = m_typeCombo->findData(Settings::archiveType());
327     if (idx!=-1) m_typeCombo->setCurrentIndex(idx);
328 
329     m_waitTimeSpinbox->setValue(Settings::waitTime());
330 
331     m_noProxyCheck->setChecked(Settings::noProxy());
332     m_randomWaitCheck->setChecked(Settings::randomWait());
333     m_fixExtensionsCheck->setChecked(Settings::fixExtensions());
334     m_runInTerminalCheck->setChecked(Settings::runInTerminal());
335     m_closeWhenFinishedCheck->setChecked(Settings::closeWhenFinished());
336 
337     m_randomWaitCheck->setEnabled(m_waitTimeSpinbox->value()>0);
338 }
339 
340 
slotCreateButtonClicked()341 void ArchiveDialog::slotCreateButtonClicked()
342 {
343     setGuiEnabled(false);				// while archiving is in progress
344     showMessage("");					// clear any existing message
345 
346     m_saveUrl = m_saveUrlReq->url();
347     qCDebug(WEBARCHIVERPLUGIN_LOG) << m_saveUrl;
348 
349     if (!m_saveUrl.isValid())
350     {
351         showMessageAndCleanup(i18nc("@info", "The save location is not valid."), KMessageWidget::Error);
352         return;
353     }
354 
355     // Remember the archive save location used
356     QUrl url = m_saveUrl.adjusted(QUrl::RemoveFilename);
357     if (url.isValid()) KRecentDirs::add(":save", url.toString(QUrl::PreferLocalFile));
358 
359     // Also save the window size and the other GUI options.
360     // From here on we can use Settings to access them.
361     saveSettings();
362 
363     // Check that the wget(1) command is available before doing too much other work
364     if (m_wgetProgram.isEmpty())
365     {
366         m_wgetProgram = QStandardPaths::findExecutable("wget");
367         qCDebug(WEBARCHIVERPLUGIN_LOG) << "wget program" << m_wgetProgram;
368         if (m_wgetProgram.isEmpty())
369         {
370             showMessageAndCleanup(xi18nc("@info",
371                                          "Cannot find the wget(1) command,<nl/>see <link>%1</link>.",
372                                          "https://www.gnu.org/software/wget"),
373                                   KMessageWidget::Error);
374             return;
375         }
376     }
377 
378     // Check whether the destination file or directory exists
379     KIO::StatJob *statJob = KIO::statDetails(m_saveUrl, KIO::StatJob::DestinationSide, KIO::StatBasic);
380     connect(statJob, &KJob::result, this, &ArchiveDialog::slotCheckedDestination);
381 }
382 
383 
slotCheckedDestination(KJob * job)384 void ArchiveDialog::slotCheckedDestination(KJob *job)
385 {
386     KIO::StatJob *statJob = qobject_cast<KIO::StatJob *>(job);
387     Q_ASSERT(statJob!=nullptr);
388 
389     const int err = job->error();
390     if (err!=0 && err!=KIO::ERR_DOES_NOT_EXIST)
391     {
392         showMessageAndCleanup(xi18nc("@info",
393                                      "Cannot verify destination<nl/><filename>%1</filename><nl/>%2",
394                                      statJob->url().toDisplayString(), job->errorString()),
395                               KMessageWidget::Error);
396         return;
397     }
398 
399     if (err==0) m_saveUrl = statJob->mostLocalUrl();	// update to most local form
400     m_saveType = m_typeCombo->itemData(m_typeCombo->currentIndex()).toString();
401     qCDebug(WEBARCHIVERPLUGIN_LOG) << m_saveUrl << "as" << m_saveType;
402 
403     if (err==0)						// destination already exists
404     {
405         const bool isDir = statJob->statResult().isDir();
406         const QString url = m_saveUrl.toDisplayString(QUrl::PreferLocalFile);
407         QString message;
408         if (m_saveType=="inode/directory")
409         {
410             if (!isDir) message = xi18nc("@info", "The archive directory<nl/><filename>%1</filename><nl/>already exists as a file.", url);
411             else message = xi18nc("@info", "The archive directory<nl/><filename>%1</filename><nl/>already exists.", url);
412         }
413         else
414         {
415             if (isDir) message = xi18nc("@info", "The archive file<nl/><filename>%1</filename><nl/>already exists as a directory.", url);
416             else message = xi18nc("@info", "The archive file <nl/><filename>%1</filename><nl/>already exists.", url);
417         }
418 
419         int result = KMessageBox::warningContinueCancel(this, message,
420                                                         i18n("Archive Already Exists"),
421                                                         KStandardGuiItem::overwrite(),
422                                                         KStandardGuiItem::cancel(),
423                                                         QString(),
424                                                         KMessageBox::Dangerous);
425         if (result==KMessageBox::Cancel)
426         {
427             showMessageAndCleanup("");
428             return;
429         }
430 
431         KIO::DeleteJob *delJob = KIO::del(m_saveUrl);
432         connect(delJob, &KJob::result, this, &ArchiveDialog::slotDeletedOldDestination);
433         return;
434     }
435 
436     slotDeletedOldDestination(nullptr);
437 }
438 
439 
slotDeletedOldDestination(KJob * job)440 void ArchiveDialog::slotDeletedOldDestination(KJob *job)
441 {
442     if (job!=nullptr)
443     {
444         KIO::DeleteJob *delJob = qobject_cast<KIO::DeleteJob *>(job);
445         Q_ASSERT(delJob!=nullptr);
446 
447         if (job->error())
448         {
449             showMessageAndCleanup(xi18nc("@info",
450                                          "Cannot delete original archive<nl/><filename>%1</filename><nl/>%2",
451                                          m_saveUrl.toDisplayString(), job->errorString()),
452                                   KMessageWidget::Error);
453             return;
454         }
455     }
456 
457     startDownloadProcess();
458 }
459 
460 
startDownloadProcess()461 void ArchiveDialog::startDownloadProcess()
462 {
463     m_tempDir = new QTemporaryDir;
464     if (!m_tempDir->isValid())
465     {
466         showMessageAndCleanup(xi18nc("@info",
467                                      "Cannot create a temporary directory<nl/><filename>%1</filename><nl/>",
468                                      m_tempDir->path()),
469                               KMessageWidget::Error);
470         return;
471     }
472     qCDebug(WEBARCHIVERPLUGIN_LOG) << "temp dir" << m_tempDir->path();
473 
474     QProcess *proc = new QProcess(this);
475     proc->setProcessChannelMode(QProcess::ForwardedChannels);
476     proc->setStandardInputFile(QProcess::nullDevice());
477     proc->setWorkingDirectory(m_tempDir->path());
478     Q_ASSERT(!m_wgetProgram.isEmpty());			// should have found this earlier
479     proc->setProgram(m_wgetProgram);
480 
481     QStringList args;					// argument list for command
482     args << "-p";					// fetch page and all requirements
483     args << "-k";					// convert to relative links
484     args << "-nH";					// do not create host directories
485     // This option is incompatible with '-k'
486     //args << "-nc";					// no clobber of existing files
487     args << "-H";					// fetch from foreign hosts
488     args << "-nv";					// not quite so verbose
489     args << "--progress=dot:default";			// progress indication
490     args << "-R" << "robots.txt";			// ignore this file
491 
492     if (Settings::fixExtensions())			// want standard archive format
493     {
494         args << "-nd";					// no subdirectory structure
495         args << "-E";					// fix up file extensions
496     }
497 
498     const int waitTime = Settings::waitTime();
499     if (waitTime>0)					// wait time requested?
500     {
501         args << "-w" << QString::number(waitTime);	// wait time between requests
502         if (Settings::randomWait())
503         {
504             args << "--random-wait";			// randomise wait time
505         }
506     }
507 
508     if (Settings::noProxy())				// no proxy requested?
509     {
510         args << "--no-proxy";				// do not use proxy
511     }
512 
513     args << m_pageUrlReq->url().toEncoded();		// finally the page URL
514 
515     qCDebug(WEBARCHIVERPLUGIN_LOG) << "wget args" << args;
516     if (Settings::runInTerminal())			// running in a terminal?
517     {
518         args.prepend(proc->program());			// prepend existing "wget"
519         args.prepend("-e");				// terminal command to execute
520         args.prepend("--hold");				// then terminal options
521 
522         // from kservice/src/kdeinit/ktoolinvocation_x11.cpp
523         KConfigGroup generalGroup(KSharedConfig::openConfig(), "General");
524         const QString term = generalGroup.readPathEntry("TerminalApplication", QStringLiteral("konsole"));
525         proc->setProgram(term);				// set terminal as program
526 
527         qCDebug(WEBARCHIVERPLUGIN_LOG) << "terminal" << term << "args" << args;
528     }
529     proc->setArguments(args);
530 
531     connect(proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
532             this, &ArchiveDialog::slotProcessFinished);
533 
534     m_archiveProcess = proc;				// note for cleanup later
535     proc->start();					// start the archiver process
536     if (!proc->waitForStarted(2000))
537     {
538         showMessageAndCleanup(xi18nc("@info",
539                                      "Cannot start the archiver process <filename>%1</filename>",
540                                      m_wgetProgram),
541                               KMessageWidget::Error);
542         return;
543     }
544 }
545 
546 
slotProcessFinished(int exitCode,QProcess::ExitStatus exitStatus)547 void ArchiveDialog::slotProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
548 {
549     qCDebug(WEBARCHIVERPLUGIN_LOG) << "code" << exitCode << "status" << exitStatus;
550 
551     QString message;
552     if (exitStatus==QProcess::CrashExit)
553     {
554         // See if we terminated the process ourselves via queryClose()
555         if (exitCode==SIGTERM) message = xi18nc("@info", "The download process was interrupted.");
556         else message = xi18nc("@info", "<para>The download process <filename>%1</filename> failed with signal %2.</para>",
557                               m_archiveProcess->program(), QString::number(exitCode));
558     }
559     else if (exitCode!=0)
560     {
561         message = xi18nc("@info", "<para>The download process <filename>%1</filename> failed with status %2.</para>",
562                          m_archiveProcess->program(), QString::number(exitCode));
563         if (exitCode==8)
564         {
565             message += xi18nc("@info", "<para>This may simply indicate a 404 error for some of the page elements.</para>");
566         }
567     }
568 
569     if (!message.isEmpty())
570     {
571         message += xi18nc("@info", "<para>Retain the partial web archive?</para>");
572         if (KMessageBox::questionYesNo(this, message, i18n("Retain Archive?"),
573                                        KGuiItem(i18n("Retain"), KStandardGuiItem::save().icon()),
574                                        KStandardGuiItem::discard())==KMessageBox::No)
575         {
576             showMessageAndCleanup(xi18nc("@info",
577                                          "Creating the web archive failed."),
578                                   KMessageWidget::Warning);
579             return;
580         }
581     }
582 
583     finishArchive();
584 }
585 
586 
finishArchive()587 void ArchiveDialog::finishArchive()
588 {
589     QDir tempDir(m_tempDir->path());			// where the download now is
590 
591     if (Settings::fixExtensions())
592     {
593         // Look at the names at the top level of the temporary download directory,
594         // and see if there is a single HTML file there.  If there is, then depending
595         // on whether the original URL ended with a slash (which we cannot check or
596         // correct because we cannot know whether it should or not), wget(1) may
597         // save the HTML page as "lastcomponent.html" instead of "index.html".
598         //
599         // If this is the case, then rename the file in question to "index.html".
600         // This is so that a web archive will open in Konqueror showing the HTML
601         // page as intended.
602         //
603         // If saving as a directory or as another type of archive file, then
604         // this does not apply.  However, do the rename anyway so that the
605         // file naming is consistent regardless of what is being saved.
606 
607         QRegExp rx("(?!^index)\\.html?$");		// a negative lookahead assertion!
608         rx.setCaseSensitivity(Qt::CaseInsensitive);
609         QString indexHtml;				// the index file found
610 
611         // This listing can simply use QDir::entryList() because only the
612         // file names are needed.
613         const QStringList entries = tempDir.entryList(QDir::Dirs|QDir::Files|QDir::QDir::NoDotAndDotDot);
614         qCDebug(WEBARCHIVERPLUGIN_LOG) << "found" << entries.count() << "entries";
615 
616         for (const QString &name : entries)		// first pass, check file names
617         {
618             if (name.contains(rx))			// matches "anythingelse.html"
619             {						// but not "index.html"
620                 if (!indexHtml.isEmpty())		// already have found something
621                 {
622                     qCDebug(WEBARCHIVERPLUGIN_LOG) << "multiple HTML files at top level";
623                     indexHtml.clear();			// forget trying to rename
624                     break;
625                 }
626 
627                 qCDebug(WEBARCHIVERPLUGIN_LOG) << "identified index file" << name;
628                 indexHtml = name;
629             }
630         }
631 
632         if (!indexHtml.isEmpty())			// have identified index file
633         {
634             tempDir.rename(indexHtml, "index.html");	// rename it to standard name
635         }
636     }
637 
638     QString sourcePath;					// archive to be copied
639 
640     // The archived web page is now ready in the temporary directory.
641     // If it is required to be saved as a file, then create a
642     // temporary archive file in the same place.
643     if (m_saveType!="inode/directory")			// saving as archive file
644     {
645         QMimeDatabase db;
646         const QString ext = db.mimeTypeForName(m_saveType).preferredSuffix();
647         m_tempFile = new QTemporaryFile(QDir::tempPath()+'/'+
648                                         QCoreApplication::applicationName()+
649                                         "-XXXXXX."+ext);
650         m_tempFile->setAutoRemove(false);
651         m_tempFile->open();
652         QString tempArchive = m_tempFile->fileName();
653         qCDebug(WEBARCHIVERPLUGIN_LOG) << "temp archive" << tempArchive;
654         m_tempFile->close();				// only want the name
655 
656         KArchive *archive;
657         if (m_saveType=="application/zip")
658         {
659             archive = new KZip(tempArchive);
660         }
661         else
662         {
663             if (m_saveType=="application/x-webarchive")
664             {
665                 // A web archive is a gzip-compressed tar file
666                 archive = new KTar(tempArchive, "application/x-gzip");
667             }
668             else archive = new KTar(tempArchive);
669         }
670         archive->open(QIODevice::WriteOnly);
671 
672         // Read each entry in the temporary directory and add it to the archive.
673         // Cannnot simply use addLocalDirectory(m_tempDir->path()) here, because
674         // that would add an extra directory level within the archive having the
675         // random name of the temporary directory.
676         //
677         // This listing needs to use QDir::entryInfoList() so that adding to the
678         // archive can distinguish between files and directories.  The list needs
679         // to be refreshed because the page HTML file could have been renamed above.
680         const QFileInfoList entries = tempDir.entryInfoList(QDir::Dirs|QDir::Files|QDir::QDir::NoDotAndDotDot);
681         qCDebug(WEBARCHIVERPLUGIN_LOG) << "adding" << entries.count() << "entries";
682 
683         for (const QFileInfo &fi : entries)		// second pass, write out entries
684         {
685             if (fi.isFile())
686             {
687                 qCDebug(WEBARCHIVERPLUGIN_LOG) << "  adding file" << fi.absoluteFilePath();
688                 archive->addLocalFile(fi.absoluteFilePath(), fi.fileName());
689             }
690             else if (fi.isDir())
691             {
692                 qCDebug(WEBARCHIVERPLUGIN_LOG) << "  adding dir" << fi.absoluteFilePath();
693                 archive->addLocalDirectory(fi.absoluteFilePath(), fi.fileName());
694             }
695             else qCDebug(WEBARCHIVERPLUGIN_LOG) << "unrecognised entry type for" << fi.fileName();
696         }
697 
698         archive->close();				// finished with archive file
699         sourcePath = tempArchive;			// source path to copy
700     }
701     else						// saving as a directory
702     {
703         sourcePath = tempDir.absolutePath();		// source path to copy
704     }
705 
706     // Finally copy the temporary file or directory to the requested save location
707     KIO::CopyJob *copyJob = KIO::copyAs(QUrl::fromLocalFile(sourcePath), m_saveUrl);
708     connect(copyJob, &KJob::result, this, &ArchiveDialog::slotCopiedArchive);
709 }
710 
711 
slotCopiedArchive(KJob * job)712 void ArchiveDialog::slotCopiedArchive(KJob *job)
713 {
714     KIO::CopyJob *copyJob = qobject_cast<KIO::CopyJob *>(job);
715     Q_ASSERT(copyJob!=nullptr);
716     const QUrl destUrl = copyJob->destUrl();
717 
718     if (job->error())
719     {
720         showMessageAndCleanup(xi18nc("@info",
721                                      "Cannot copy archive to<nl/><filename>%1</filename><nl/>%2",
722                                      destUrl.toDisplayString(), job->errorString()),
723                               KMessageWidget::Error);
724         return;
725     }
726 
727     // Explicitly set permissions on the saved archive file or directory,
728     // to honour the user's umask(2) setting.  This is needed because
729     // both QTemporaryFile and QTemporaryDir create them with restrictive
730     // permissions (as indeed they should) by default.  The files within
731     // the temporary directory will have been written by wget(1) with
732     // standard creation permissions, so it does not need to be done
733     // recursively.
734     const mode_t perms = (m_saveType=="inode/directory") ? 0777 : 0666;
735     const mode_t mask = umask(0); umask(mask);
736 
737     KIO::SimpleJob *chmodJob = KIO::chmod(destUrl, (perms & ~mask));
738     connect(chmodJob, &KJob::result, this, &ArchiveDialog::slotFinishedArchive);
739 }
740 
741 
slotFinishedArchive(KJob * job)742 void ArchiveDialog::slotFinishedArchive(KJob *job)
743 {
744     KIO::SimpleJob *chmodJob = qobject_cast<KIO::SimpleJob *>(job);
745     Q_ASSERT(chmodJob!=nullptr);
746     const QUrl destUrl = chmodJob->url();
747 
748     if (job->error())
749     {
750         showMessageAndCleanup(xi18nc("@info",
751                                      "Cannot set permissions on<nl/><filename>%1</filename><nl/>%2",
752                                      destUrl.toDisplayString(), job->errorString()),
753                               KMessageWidget::Warning);
754         return;						// do not close even if requested
755     }
756     else
757     {
758         showMessageAndCleanup(xi18nc("@info",
759                                      "Web archive saved as<nl/><filename><link>%1</link></filename>",
760                                      destUrl.toDisplayString()),
761                               KMessageWidget::Positive);
762     }
763 
764     // Now the archiving task is finished.
765     if (Settings::closeWhenFinished())
766     {
767         // Let the user briefly see the completion message.
768         QTimer::singleShot(1000, qApp, &QCoreApplication::quit);
769     }
770 }
771 
772 
showMessageAndCleanup(const QString & text,KMessageWidget::MessageType type)773 void ArchiveDialog::showMessageAndCleanup(const QString &text, KMessageWidget::MessageType type)
774 {
775     showMessage(text, type);
776     cleanup();
777     setGuiEnabled(true);
778 }
779 
780 
showMessage(const QString & text,KMessageWidget::MessageType type)781 void ArchiveDialog::showMessage(const QString &text, KMessageWidget::MessageType type)
782 {
783     if (text.isEmpty())					// remove existing message
784     {
785         m_messageWidget->hide();
786         return;
787     }
788 
789     QString iconName;
790     switch (type)
791     {
792 case KMessageWidget::Positive:		iconName = "dialog-ok";			break;
793 case KMessageWidget::Information:	iconName = "dialog-information";	break;
794 default:
795 case KMessageWidget::Warning:		iconName = "dialog-warning";		break;
796 case KMessageWidget::Error:		iconName = "dialog-error";		break;
797     }
798 
799     m_messageWidget->setCloseButtonVisible(type!=KMessageWidget::Positive && type!=KMessageWidget::Information);
800     m_messageWidget->setIcon(QIcon::fromTheme(iconName));
801     m_messageWidget->setMessageType(type);
802     m_messageWidget->setText(text);
803     m_messageWidget->show();
804 }
805