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