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 }