diff --git a/host/CMakeLists.txt b/host/CMakeLists.txt
index fbe4345c..3d92c6ab 100644
--- a/host/CMakeLists.txt
+++ b/host/CMakeLists.txt
@@ -1,37 +1,38 @@
add_definitions(-DTRANSLATION_DOMAIN=\"plasma-browser-integration-host\")
configure_file(config-host.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-host.h)
set(HOST_SOURCES main.cpp
connection.cpp
pluginmanager.cpp
settings.cpp
mprisplugin.cpp
abstractbrowserplugin.cpp
kdeconnectplugin.cpp
downloadplugin.cpp
downloadjob.cpp
tabsrunnerplugin.cpp
purposeplugin.cpp
)
qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.kde.plasma.browser_integration.TabsRunner.xml tabsrunnerplugin.h TabsRunnerPlugin)
qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.kde.plasma.browser_integration.Settings.xml settings.h Settings)
qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.mpris.MediaPlayer2.xml mprisplugin.h MPrisPlugin mprisroot MPrisRoot)
qt5_add_dbus_adaptor(HOST_SOURCES ../dbus/org.mpris.MediaPlayer2.Player.xml mprisplugin.h MPrisPlugin mprisplayer MPrisPlayer)
add_executable(plasma-browser-integration-host ${HOST_SOURCES})
target_link_libraries(
plasma-browser-integration-host
Qt5::DBus
Qt5::Gui
Qt5::Widgets
+ KF5::Activities
KF5::Crash
KF5::I18n
KF5::KIOCore
KF5::PurposeWidgets
KF5::FileMetaData
)
install(TARGETS plasma-browser-integration-host ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
diff --git a/host/downloadjob.cpp b/host/downloadjob.cpp
index e4982a8f..efcb857d 100644
--- a/host/downloadjob.cpp
+++ b/host/downloadjob.cpp
@@ -1,278 +1,299 @@
/*
Copyright (C) 2017 by Kai Uwe Broulik
Copyright (C) 2017 by David Edmundson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#include "downloadjob.h"
#include "settings.h"
#include
+#include
#include
+#include
#include
#include
#include
DownloadJob::DownloadJob(int id)
: KJob()
, m_id(id)
{
// the thing with "canResume" in chrome downloads is that it just means
// "this download can be resumed right now because it is paused",
// it's not a general thing. I think we can always pause/resume downloads
// unless they're canceled/interrupted at which point we don't have a DownloadJob
// anymore anyway
setCapabilities(Killable | Suspendable);
// TODO When suspending on Firefox the download job goes away for some reason?!
// Until I have the virtue to figure that out just disallow suspending downloads on Firefox :)
if (Settings::self().environment() == Settings::Environment::Firefox) {
setCapabilities(Killable);
}
}
void DownloadJob::start()
{
QMetaObject::invokeMethod(this, "doStart", Qt::QueuedConnection);
}
void DownloadJob::doStart()
{
}
bool DownloadJob::doKill()
{
emit killRequested();
// TODO what if the user kills us from notification area while the
// "Save As" prompt is still open?
return true;
}
bool DownloadJob::doSuspend()
{
emit suspendRequested();
return true;
}
bool DownloadJob::doResume()
{
emit resumeRequested();
return true;
}
void DownloadJob::update(const QJsonObject &payload)
{
auto end = payload.constEnd();
bool descriptionDirty = false;
auto it = payload.constFind(QStringLiteral("url"));
if (it != end) {
m_url = QUrl(it->toString());
descriptionDirty = true; // TODO only if actually changed
}
it = payload.constFind(QStringLiteral("finalUrl"));
if (it != end) {
m_finalUrl = QUrl(it->toString());
descriptionDirty = true;
}
it = payload.constFind(QStringLiteral("filename"));
if (it != end) {
m_fileName = it->toString();
const QUrl destination = QUrl::fromLocalFile(it->toString());
setProperty("destUrl", destination.toString(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
m_destination = destination;
descriptionDirty = true;
}
it = payload.constFind(QStringLiteral("mime"));
if (it != end) {
m_mimeType = it->toString();
}
it = payload.constFind(QStringLiteral("incognito"));
if (it != end) {
m_incognito = it->toBool();
}
it = payload.constFind(QStringLiteral("totalBytes"));
if (it != end) {
const qlonglong totalAmount = it->toDouble();
if (totalAmount > -1) {
setTotalAmount(Bytes, totalAmount);
}
}
it = payload.constFind(QStringLiteral("bytesReceived"));
if (it != end) {
setProcessedAmount(Bytes, it->toDouble());
}
setTotalAmount(Files, 1);
it = payload.constFind(QStringLiteral("paused"));
if (it != end) {
const bool paused = it->toBool();
if (paused) {
suspend();
} else {
resume();
}
}
it = payload.constFind(QStringLiteral("estimatedEndTime"));
if (it != end) {
qulonglong speed = 0;
// now calculate the speed from estimated end time and total size
// funny how chrome only gives us a time whereas KJob operates on speed
// and calculates the time this way :)
const QDateTime endTime = QDateTime::fromString(it->toString(), Qt::ISODate);
if (endTime.isValid()) {
const QDateTime now = QDateTime::currentDateTimeUtc();
qulonglong remainingBytes = totalAmount(Bytes) - processedAmount(Bytes);
quint64 remainingTime = now.secsTo(endTime);
if (remainingTime > 0) {
speed = remainingBytes / remainingTime;
}
}
emitSpeed(speed);
}
if (descriptionDirty) {
updateDescription();
}
const QString error = payload.value(QStringLiteral("error")).toString();
if (!error.isEmpty()) {
if (error == QLatin1String("USER_CANCELED")
|| error == QLatin1String("USER_SHUTDOWN")) {
setError(KIO::ERR_USER_CANCELED); // will keep Notification applet from showing a "finished"/error message
emitResult();
return;
}
// value is a QVariant so we can be lazy and support both KIO errors and custom test
// if QVariant is an int: use that as KIO error
// if QVariant is a QString: set UserError and message
static const QHash errors {
// for a list of these error codes *and their meaning* instead of looking at browser
// extension docs, check out Chromium's source code: download_interrupt_reason_values.h
{QStringLiteral("FILE_ACCESS_DENIED"), i18n("Access denied.")}, // KIO::ERR_ACCESS_DENIED
{QStringLiteral("FILE_NO_SPACE"), i18n("Insufficient free space.")}, // KIO::ERR_DISK_FULL
{QStringLiteral("FILE_NAME_TOO_LONG"), i18n("The file name you have chosen is too long.")},
{QStringLiteral("FILE_TOO_LARGE"), i18n("The file is too large to be downloaded.")},
// haha
{QStringLiteral("FILE_VIRUS_INFECTED"), i18n("The file possibly contains malicious contents.")},
{QStringLiteral("FILE_TRANSIENT_ERROR"), i18n("A temporary error has occurred. Please try again later.")},
{QStringLiteral("NETWORK_FAILED"), i18n("A network error has occurred.")},
{QStringLiteral("NETWORK_TIMEOUT"), i18n("The network operation timed out.")}, // TODO something less geeky
{QStringLiteral("NETWORK_DISCONNECTED"), i18n("The network connection has been lost.")},
{QStringLiteral("NETWORK_SERVER_DOWN"), i18n("The server is no longer reachable.")},
{QStringLiteral("SERVER_FAILED"), i18n("A server error has occurred.")},
// chromium code says "internal use" and this is really not something the user should see
// SERVER_NO_RANGE"
// SERVER_PRECONDITION
{QStringLiteral("SERVER_BAD_CONTENT"), i18n("The server does not have the requested data.")},
{QStringLiteral("CRASH"), i18n("The browser application closed unexpectedly.")}
};
const QString &errorValue = errors.value(error);
if (errorValue.isEmpty()) { // unknown error
setError(KIO::ERR_UNKNOWN);
setErrorText(i18n("An unknown error occurred while downloading."));
emitResult();
return;
}
// KIO::Error doesn't have a UserDefined one, let's just use magic numbers then
// TODO at least set the KIO::Errors that we do have
setError(1000);
setErrorText(errorValue);
emitResult();
return;
}
it = payload.constFind(QStringLiteral("state"));
if (it != end) {
const QString state = it->toString();
// We ignore "interrupted" state and only cancel if we get supplied an "error"
if (state == QLatin1String("complete")) {
setError(KJob::NoError);
setProcessedAmount(KJob::Files, 1);
+ // Add to recent document
+ addToRecentDocuments();
+
// Write origin url into extended file attributes
saveOriginUrl();
emitResult();
return;
}
}
}
void DownloadJob::updateDescription()
{
description(this, i18nc("Job heading, like 'Copying'", "Downloading"),
qMakePair(i18nc("The URL being downloaded", "Source"), (m_finalUrl.isValid() ? m_finalUrl : m_url).toDisplayString()),
qMakePair(i18nc("The location being downloaded to", "Destination"), m_destination.toLocalFile())
);
}
+void DownloadJob::addToRecentDocuments()
+{
+ if (m_incognito || m_fileName.isEmpty()) {
+ return;
+ }
+
+ const QJsonObject settings = Settings::self().settingsForPlugin(QStringLiteral("downloads"));
+
+ const bool enabled = settings.value(QStringLiteral("addToRecentDocuments")).toBool();
+ if (!enabled) {
+ return;
+ }
+
+ KActivities::ResourceInstance::notifyAccessed(QUrl::fromLocalFile(m_fileName), qApp->desktopFileName());
+}
+
void DownloadJob::saveOriginUrl()
{
if (m_incognito
// Blob URLs are dynamically created through JavaScript and cannot be accessed from the outside
|| m_finalUrl.scheme() == QLatin1String("blob")) {
return;
}
const QJsonObject settings = Settings::self().settingsForPlugin(QStringLiteral("downloads"));
const bool saveOriginUrl = settings.value(QStringLiteral("saveOriginUrl")).toBool();
if (!saveOriginUrl) {
return;
}
KFileMetaData::UserMetaData md(m_fileName);
QUrl url = m_finalUrl;
url.setPassword(QString());
md.setOriginUrl(url);
}
diff --git a/host/downloadjob.h b/host/downloadjob.h
index 957b34e3..412fc059 100644
--- a/host/downloadjob.h
+++ b/host/downloadjob.h
@@ -1,79 +1,80 @@
/*
Copyright (C) 2017 by Kai Uwe Broulik
Copyright (C) 2017 by David Edmundson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#pragma once
#include
#include
class DownloadJob : public KJob
{
Q_OBJECT
public:
DownloadJob(int id);
enum class State {
None,
InProgress,
Interrupted,
Complete
};
void start() override;
void update(const QJsonObject &payload);
Q_SIGNALS:
void killRequested();
void suspendRequested();
void resumeRequested();
private Q_SLOTS:
void doStart();
protected:
bool doKill() override;
bool doSuspend() override;
bool doResume() override;
private:
void updateDescription();
+ void addToRecentDocuments();
void saveOriginUrl();
int m_id = -1;
QUrl m_url;
QUrl m_finalUrl;
QUrl m_destination;
QString m_fileName;
QString m_mimeType;
// In doubt, assume incognito
bool m_incognito = true;
};
diff --git a/host/downloadplugin.cpp b/host/downloadplugin.cpp
index a4e28b00..68f7ba74 100644
--- a/host/downloadplugin.cpp
+++ b/host/downloadplugin.cpp
@@ -1,116 +1,116 @@
/*
Copyright (C) 2017 by Kai Uwe Broulik
Copyright (C) 2017 by David Edmundson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#include "downloadplugin.h"
#include "connection.h"
#include "downloadjob.h"
#include
#include
DownloadPlugin::DownloadPlugin(QObject* parent) :
- AbstractBrowserPlugin(QStringLiteral("downloads"), 2, parent)
+ AbstractBrowserPlugin(QStringLiteral("downloads"), 3, parent)
{
}
bool DownloadPlugin::onLoad()
{
// Have extension tell us about all the downloads
sendData(QStringLiteral("createAll"));
return true;
}
bool DownloadPlugin::onUnload()
{
for (auto it = m_jobs.constBegin(), end = m_jobs.constEnd(); it != end; ++it) {
it.value()->deleteLater(); // kill() would abort the download
}
return true;
}
void DownloadPlugin::handleData(const QString& event, const QJsonObject& payload)
{
const QJsonObject &download = payload.value(QStringLiteral("download")).toObject();
const int id = download.value(QStringLiteral("id")).toInt(-1);
if (id < 0) {
qWarning() << "Cannot update download with invalid id" << id;
return;
}
if (event == QLatin1String("created")) {
// If we get a created event for an already existing job, update it instead
auto *job = m_jobs.value(id);
if (job) {
job->update(download);
return;
}
job = new DownloadJob(id);
// first register and then update, otherwise we miss the initial population..
KIO::getJobTracker()->registerJob(job);
job->update(download);
m_jobs.insert(id, job);
connect(job, &DownloadJob::killRequested, this, [this, id] {
sendData(QStringLiteral("cancel"), {
{QStringLiteral("downloadId"), id}
});
});
connect(job, &DownloadJob::suspendRequested, this, [this, id] {
sendData(QStringLiteral("suspend"), {
{QStringLiteral("downloadId"), id}
});
});
connect(job, &DownloadJob::resumeRequested, this, [this, id] {
sendData(QStringLiteral("resume"), {
{QStringLiteral("downloadId"), id}
});
});
QObject::connect(job, &QObject::destroyed, this, [this, id] {
m_jobs.remove(id);
});
job->start();
QObject::connect(job, &KJob::finished, this, [this, job, id] {
});
} else if (event == QLatin1String("update")) {
auto *job = m_jobs.value(id);
if (!job) {
debug() << "Failed to find download to update with id" << id;
return;
}
job->update(download);
}
}