diff --git a/CMakeLists.txt b/CMakeLists.txt index 60b052893..7c8ff1853 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,6 +57,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) set(DESKTOP_FILE morph-browser.desktop) +set(PUSH_HELPER_DIR "lib/ubuntu-push-client/legacy-helpers") # uninstall target configure_file( @@ -94,4 +95,6 @@ add_subdirectory(doc) install(FILES morph-browser.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/morph-browser) install(FILES morph-browser-splash.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/morph-browser) +install(PROGRAMS push-helper/morph-browser-helper.sh DESTINATION ${PUSH_HELPER_DIR} RENAME morph-browser) + add_subdirectory(click-hooks) diff --git a/debian/morph-browser-apparmor.manifest b/debian/morph-browser-apparmor.manifest index 20da2280f..2d90b2219 100644 --- a/debian/morph-browser-apparmor.manifest +++ b/debian/morph-browser-apparmor.manifest @@ -26,7 +26,7 @@ ], "template_variables": { "APP_ID_DBUS": "morph_2dbrowser", - "APP_PKGNAME_DBUS": "morph_2dbrowser", + "APP_PKGNAME_DBUS": "_", "APP_PKGNAME": "morph-browser", "CLICK_DIR": "/usr/share/morph-browser" }, diff --git a/debian/morph-browser.install b/debian/morph-browser.install index eae71a260..de61bddc8 100644 --- a/debian/morph-browser.install +++ b/debian/morph-browser.install @@ -1,4 +1,5 @@ usr/bin/morph-browser +usr/lib/ubuntu-push-client/legacy-helpers usr/share/morph-browser/*.qml usr/share/morph-browser/qmldir usr/share/morph-browser/*.js diff --git a/push-helper/morph-browser-helper.sh b/push-helper/morph-browser-helper.sh new file mode 100644 index 000000000..5ebbc4dfe --- /dev/null +++ b/push-helper/morph-browser-helper.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +cp $1 $2 diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index d6f653425..a22366661 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -30,6 +30,8 @@ set(COMMONLIB_SRC input-method-handler.cpp meminfo.cpp mime-database.cpp + notifications-proxy.cpp + pushclient/pushclient.cpp session-storage.cpp single-instance-manager.cpp ) @@ -44,6 +46,7 @@ target_link_libraries(${COMMONLIB} Qt5::Qml Qt5::Quick Qt5::Widgets + Qt5DBus Qt5WebEngine Qt5WebEngineCore ${LIBAPPARMOR_LDFLAGS} diff --git a/src/app/DomainSettingsPage.qml b/src/app/DomainSettingsPage.qml index a19c33c9e..b74b60a10 100644 --- a/src/app/DomainSettingsPage.qml +++ b/src/app/DomainSettingsPage.qml @@ -171,6 +171,7 @@ FocusScope { readonly property string domain: model.domain readonly property int userAgentId: model.userAgentId readonly property int locationPreference: model.allowLocation + readonly property int notificationsPreference: model.allowNotifications height: isCurrentItem ? layout.height : units.gu(5) color: isCurrentItem ? ((theme.palette.selected.background.hslLightness > 0.5) ? Qt.darker(theme.palette.selected.background, 1.05) : Qt.lighter(theme.palette.selected.background, 1.5)) : theme.palette.normal.background @@ -210,12 +211,11 @@ FocusScope { CheckBox { checked: model.allowCustomUrlSchemes - onTriggered: DomainSettingsModel.allowCustomUrlSchemes(model.domain, checked) + onTriggered: DomainSettingsModel.allowCustomUrlSchemes(model.domain, checked, false) anchors.verticalCenter: parent.verticalCenter } } - Row { spacing: units.gu(1.5) height: units.gu(1) @@ -234,6 +234,25 @@ FocusScope { anchors.verticalCenter: parent.verticalCenter } } + + Row { + spacing: units.gu(1.5) + height: units.gu(1) + visible: item.ListView.isCurrentItem + + Label { + width: parent.width * 0.5 + text: i18n.tr("send notifications") + anchors.verticalCenter: parent.verticalCenter + } + + ComboBox { + model: [ i18n.tr("Ask each time"), i18n.tr("Allowed"), i18n.tr("Denied") ] + currentIndex: item.notificationsPreference + onCurrentIndexChanged: DomainSettingsModel.setNotificationsPreference(item.domain, currentIndex, false) + anchors.verticalCenter: parent.verticalCenter + } + } Row { spacing: units.gu(1.5) diff --git a/src/app/NotificationsAccessDialog.qml b/src/app/NotificationsAccessDialog.qml new file mode 100644 index 000000000..ac15fceff --- /dev/null +++ b/src/app/NotificationsAccessDialog.qml @@ -0,0 +1,61 @@ +/* + * Copyright 2020 UBports Foundation + * + * This file is part of morph-browser. + * + * morph-browser is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * morph-browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import Ubuntu.Components.Popups 1.3 + +Dialog { + id: dialog + + property string securityOrigin + property bool showRememberDecisionCheckBox + modal: true + + title: i18n.tr("Permission Request") + text: securityOrigin + "
" + i18n.tr("This page wants to create notifications.") + + signal allow() + signal allowPermanently() + signal reject() + signal rejectPermanently() + + onAllow: { PopupUtils.close(dialog); } + onAllowPermanently: { PopupUtils.close(dialog); } + onReject: { PopupUtils.close(dialog); } + onRejectPermanently: { PopupUtils.close(dialog); } + + ListItemLayout { + visible: showRememberDecisionCheckBox + title.text: i18n.tr("Remember decision") + CheckBox { + id: rememberDecisionCheckBox + } + } + Button { + objectName: "allow" + text: i18n.tr("Allow") + color: theme.palette.normal.positive + onClicked: rememberDecisionCheckBox.checked ? allowPermanently() : allow() + } + Button { + objectName: "deny" + text: i18n.tr("Deny") + onClicked: rememberDecisionCheckBox.checked ? rejectPermanently() : reject() + } +} diff --git a/src/app/WebViewImpl.qml b/src/app/WebViewImpl.qml index c49a47009..ff5aa9248 100644 --- a/src/app/WebViewImpl.qml +++ b/src/app/WebViewImpl.qml @@ -231,6 +231,44 @@ WebView { mediaAccessDialog.origin = securityOrigin; mediaAccessDialog.feature = feature; break; + + case WebEngineView.Notifications: + + var domain = UrlUtils.extractHost(securityOrigin); + var notificationsPreference = DomainSettingsModel.getNotificationsPreference(domain); + + if (notificationsPreference === DomainSettingsModel.AllowNotificationsAccess) + { + grantFeaturePermission(securityOrigin, feature, true); + return; + } + + if (notificationsPreference === DomainSettingsModel.DenyNotificationsAccess) + { + grantFeaturePermission(securityOrigin, feature, false); + return; + } + + var notificationsAccessDialog = PopupUtils.open(Qt.resolvedUrl("NotificationsAccessDialog.qml"), this); + notificationsAccessDialog.securityOrigin = securityOrigin; + notificationsAccessDialog.showRememberDecisionCheckBox = (domain !== "") && ! incognito + notificationsAccessDialog.allow.connect(function() { + grantFeaturePermission(securityOrigin, feature, true); + DomainSettingsModel.setNotificationsPreference(domain, DomainSettingsModel.AllowNotificationsAccess, true); + }); + notificationsAccessDialog.allowPermanently.connect(function() { + grantFeaturePermission(securityOrigin, feature, true); + DomainSettingsModel.setNotificationsPreference(domain, DomainSettingsModel.AllowNotificationsAccess, false); + }); + notificationsAccessDialog.reject.connect(function() { + grantFeaturePermission(securityOrigin, feature, false); + DomainSettingsModel.setNotificationsPreference(domain, DomainSettingsModel.DenyNotificationsAccess, true); + }); + notificationsAccessDialog.rejectPermanently.connect(function() { + grantFeaturePermission(securityOrigin, feature, false); + DomainSettingsModel.setNotificationsPreference(domain, DomainSettingsModel.DenyNotificationsAccess, false); + }); + break; } } diff --git a/src/app/browserapplication.cpp b/src/app/browserapplication.cpp index 0c44835e4..5faf827d7 100644 --- a/src/app/browserapplication.cpp +++ b/src/app/browserapplication.cpp @@ -45,6 +45,7 @@ #include "input-method-handler.h" #include "meminfo.h" #include "mime-database.h" +#include "notifications-proxy.h" #include "session-storage.h" BrowserApplication::BrowserApplication(int& argc, char** argv) @@ -107,6 +108,7 @@ MAKE_SINGLETON_FACTORY(DownloadsModel) MAKE_SINGLETON_FACTORY(FileOperations) MAKE_SINGLETON_FACTORY(MemInfo) MAKE_SINGLETON_FACTORY(MimeDatabase) +MAKE_SINGLETON_FACTORY(NotificationsProxy) MAKE_SINGLETON_FACTORY(UserAgentsModel) bool BrowserApplication::initialize(const QString& qmlFileSubPath @@ -202,6 +204,7 @@ bool BrowserApplication::initialize(const QString& qmlFileSubPath qmlRegisterSingletonType(uri, 0, 1, "FileOperations", FileOperations_singleton_factory); qmlRegisterSingletonType(uri, 0, 1, "MemInfo", MemInfo_singleton_factory); qmlRegisterSingletonType(uri, 0, 1, "MimeDatabase", MimeDatabase_singleton_factory); + qmlRegisterSingletonType(uri, 0, 1, "NotificationsProxy", NotificationsProxy_singleton_factory); qmlRegisterType(uri, 0, 1, "SessionStorage"); qmlRegisterSingletonType(uri, 0, 1, "UserAgentsModel", UserAgentsModel_singleton_factory); diff --git a/src/app/domain-settings-model.cpp b/src/app/domain-settings-model.cpp index 95cd1cb33..f53b235fa 100644 --- a/src/app/domain-settings-model.cpp +++ b/src/app/domain-settings-model.cpp @@ -71,6 +71,7 @@ QHash DomainSettingsModel::roleNames() const roles[DomainWithoutSubdomain] = "domainWithoutSubdomain"; roles[AllowCustomUrlSchemes] = "allowCustomUrlSchemes"; roles[AllowLocation] = "allowLocation"; + roles[AllowNotifications] = "allowNotifications"; roles[UserAgentId] = "userAgentId"; roles[ZoomFactor] = "zoomFactor"; } @@ -98,6 +99,8 @@ QVariant DomainSettingsModel::data(const QModelIndex& index, int role) const return entry.allowCustomUrlSchemes; case AllowLocation: return entry.allowLocation; + case AllowNotifications: + return entry.allowNotifications; case UserAgentId: return entry.userAgentId; case ZoomFactor: @@ -111,16 +114,40 @@ void DomainSettingsModel::createOrAlterDatabaseSchema() { QSqlQuery createQuery(m_database); QString query = QLatin1String("CREATE TABLE IF NOT EXISTS domainsettings " - "(domain VARCHAR NOT NULL UNIQUE, domainWithoutSubdomain VARCHAR, allowCustomUrlSchemes BOOL, allowLocation INTEGER, " + "(domain VARCHAR NOT NULL UNIQUE, domainWithoutSubdomain VARCHAR, allowCustomUrlSchemes BOOL, allowLocation INTEGER, allowNotifications INTEGER, " "userAgentId INTEGER, zoomFactor REAL, PRIMARY KEY(domain), FOREIGN KEY(userAgentId) REFERENCES useragents(id)); "); createQuery.prepare(query); createQuery.exec(); + + // Older version of the database schema didn’t have the column 'allowNotifications' + QSqlQuery tableInfoQuery(m_database); + query = QLatin1String("PRAGMA TABLE_INFO(domainsettings);"); + tableInfoQuery.prepare(query); + tableInfoQuery.exec(); + + bool missingAllowNotificationsColumn = true; + + while (tableInfoQuery.next()) { + if (tableInfoQuery.value("name").toString() == "allowNotifications") { + missingAllowNotificationsColumn = false; + } + if (!missingAllowNotificationsColumn) { + break; + } + } + + if (missingAllowNotificationsColumn) { + QSqlQuery addFolderColumnQuery(m_database); + query = QLatin1String("ALTER TABLE domainsettings ADD COLUMN allowNotifications INTEGER;"); + addFolderColumnQuery.prepare(query); + addFolderColumnQuery.exec(); + } } void DomainSettingsModel::populateFromDatabase() { QSqlQuery populateQuery(m_database); - QString query = QLatin1String("SELECT domain, domainWithoutSubdomain, allowCustomUrlSchemes, allowLocation, userAgentId, zoomFactor " + QString query = QLatin1String("SELECT domain, domainWithoutSubdomain, allowCustomUrlSchemes, allowLocation, allowNotifications, userAgentId, zoomFactor " "FROM domainsettings;"); populateQuery.prepare(query); populateQuery.exec(); @@ -131,6 +158,7 @@ void DomainSettingsModel::populateFromDatabase() entry.domainWithoutSubdomain = populateQuery.value("domainWithoutSubdomain").toString(); entry.allowCustomUrlSchemes = populateQuery.value("allowCustomUrlSchemes").toBool(); entry.allowLocation = static_cast(populateQuery.value("allowLocation").toInt()); + entry.allowNotifications = static_cast(populateQuery.value("allowNotifications").toInt()); entry.userAgentId = populateQuery.value("userAgentId").toInt(); entry.zoomFactor = populateQuery.value("zoomFactor").isNull() ? std::numeric_limits::quiet_NaN() : populateQuery.value("zoomFactor").toDouble(); @@ -194,9 +222,9 @@ bool DomainSettingsModel::areCustomUrlSchemesAllowed(const QString& domain) return m_entries[index].allowCustomUrlSchemes; } -void DomainSettingsModel::allowCustomUrlSchemes(const QString& domain, bool allow) +void DomainSettingsModel::allowCustomUrlSchemes(const QString& domain, bool allow, bool incognito) { - insertEntry(domain); + insertEntry(domain, incognito); int index = getIndexForDomain(domain); if (index != -1) { @@ -206,12 +234,15 @@ void DomainSettingsModel::allowCustomUrlSchemes(const QString& domain, bool allo } entry.allowCustomUrlSchemes = allow; Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector() << AllowCustomUrlSchemes); - QSqlQuery query(m_database); - static QString updateStatement = QLatin1String("UPDATE domainsettings SET allowCustomUrlSchemes=? WHERE domain=?;"); - query.prepare(updateStatement); - query.addBindValue(allow); - query.addBindValue(domain); - query.exec(); + if (!incognito) + { + QSqlQuery query(m_database); + static QString updateStatement = QLatin1String("UPDATE domainsettings SET allowCustomUrlSchemes=? WHERE domain=?;"); + query.prepare(updateStatement); + query.addBindValue(allow); + query.addBindValue(domain); + query.exec(); + } } } @@ -228,7 +259,7 @@ DomainSettingsModel::AllowLocationPreference DomainSettingsModel::getLocationPre void DomainSettingsModel::setLocationPreference(const QString& domain, DomainSettingsModel::AllowLocationPreference preference) { - insertEntry(domain); + insertEntry(domain, false); int index = getIndexForDomain(domain); if (index != -1) { @@ -247,6 +278,42 @@ void DomainSettingsModel::setLocationPreference(const QString& domain, DomainSet } } +DomainSettingsModel::NotificationsPreference DomainSettingsModel::getNotificationsPreference(const QString& domain) const +{ + int index = getIndexForDomain(domain); + if (index == -1) + { + return NotificationsPreference::AskForNotificationsAccess; + } + + return m_entries[index].allowNotifications; +} + +void DomainSettingsModel::setNotificationsPreference(const QString& domain, DomainSettingsModel::NotificationsPreference preference, bool incognito) +{ + insertEntry(domain, incognito); + + int index = getIndexForDomain(domain); + if (index != -1) { + DomainSetting& entry = m_entries[index]; + if (entry.allowNotifications == preference) { + return; + } + entry.allowNotifications = preference; + Q_EMIT dataChanged(this->index(index, 0), this->index(index, 0), QVector() << AllowNotifications); + + if (!incognito) + { + QSqlQuery query(m_database); + static QString updateStatement = QLatin1String("UPDATE domainsettings SET allowNotifications=? WHERE domain=?;"); + query.prepare(updateStatement); + query.addBindValue(entry.allowNotifications); + query.addBindValue(domain); + query.exec(); + } + } +} + int DomainSettingsModel::getUserAgentId(const QString& domain) const { int index = getIndexForDomain(domain); @@ -260,7 +327,7 @@ int DomainSettingsModel::getUserAgentId(const QString& domain) const void DomainSettingsModel::setUserAgentId(const QString& domain, int userAgentId) { - insertEntry(domain); + insertEntry(domain, false); int index = getIndexForDomain(domain); if (index != -1) { @@ -314,7 +381,7 @@ double DomainSettingsModel::getZoomFactor(const QString& domain) const void DomainSettingsModel::setZoomFactor(const QString& domain, double zoomFactor) { - insertEntry(domain); + insertEntry(domain, false); int index = getIndexForDomain(domain); if (index != -1) { @@ -334,7 +401,7 @@ void DomainSettingsModel::setZoomFactor(const QString& domain, double zoomFactor } } -void DomainSettingsModel::insertEntry(const QString &domain) +void DomainSettingsModel::insertEntry(const QString &domain, bool incognito) { if (contains(domain)) { @@ -347,23 +414,28 @@ void DomainSettingsModel::insertEntry(const QString &domain) entry.domainWithoutSubdomain = DomainUtils::getDomainWithoutSubdomain(domain); entry.allowCustomUrlSchemes = false; entry.allowLocation = AllowLocationPreference::AskForLocationAccess; + entry.allowNotifications = NotificationsPreference::AskForNotificationsAccess; entry.userAgentId = 0; entry.zoomFactor = std::numeric_limits::quiet_NaN(); m_entries.append(entry); endInsertRows(); Q_EMIT rowCountChanged(); - QSqlQuery query(m_database); - static QString insertStatement = QLatin1String("INSERT INTO domainsettings (domain, domainWithoutSubdomain, allowCustomUrlSchemes, allowLocation, userAgentId, zoomFactor)" - " VALUES (?, ?, ?, ?, ?, ?);"); - query.prepare(insertStatement); - query.addBindValue(entry.domain); - query.addBindValue(entry.domainWithoutSubdomain); - query.addBindValue(entry.allowCustomUrlSchemes); - query.addBindValue(entry.allowLocation); - query.addBindValue((entry.userAgentId > 0) ? entry.userAgentId : QVariant()); - query.addBindValue(entry.zoomFactor); - query.exec(); + if (!incognito) + { + QSqlQuery query(m_database); + static QString insertStatement = QLatin1String("INSERT INTO domainsettings (domain, domainWithoutSubdomain, allowCustomUrlSchemes, allowLocation, allowNotifications, userAgentId, zoomFactor)" + " VALUES (?, ?, ?, ?, ?, ?, ?);"); + query.prepare(insertStatement); + query.addBindValue(entry.domain); + query.addBindValue(entry.domainWithoutSubdomain); + query.addBindValue(entry.allowCustomUrlSchemes); + query.addBindValue(entry.allowLocation); + query.addBindValue(entry.allowNotifications); + query.addBindValue((entry.userAgentId > 0) ? entry.userAgentId : QVariant()); + query.addBindValue(entry.zoomFactor); + query.exec(); + } } void DomainSettingsModel::removeEntry(const QString &domain) @@ -390,10 +462,11 @@ void DomainSettingsModel::removeEntry(const QString &domain) void DomainSettingsModel::removeObsoleteEntries() { QSqlQuery query(m_database); - static QString deleteStatement = QLatin1String("DELETE FROM domainsettings WHERE allowCustomUrlSchemes=? AND allowLocation=? AND userAgentId IS NULL AND zoomFactor IS NULL;"); + static QString deleteStatement = QLatin1String("DELETE FROM domainsettings WHERE allowCustomUrlSchemes=? AND allowLocation=? AND allowNotifications=? AND userAgentId IS NULL AND zoomFactor IS NULL;"); query.prepare(deleteStatement); query.addBindValue(false); - query.addBindValue(false); + query.addBindValue(AllowLocationPreference::AskForLocationAccess); + query.addBindValue(NotificationsPreference::AskForNotificationsAccess); query.exec(); } diff --git a/src/app/domain-settings-model.h b/src/app/domain-settings-model.h index d3271584c..e84fc8019 100644 --- a/src/app/domain-settings-model.h +++ b/src/app/domain-settings-model.h @@ -33,6 +33,7 @@ class DomainSettingsModel : public QAbstractListModel Q_PROPERTY(double defaultZoomFactor READ defaultZoomFactor WRITE setDefaultZoomFactor) Q_ENUMS(AllowLocationPreference) + Q_ENUMS(NotificationsPreference) Q_ENUMS(Roles) public: @@ -45,11 +46,18 @@ class DomainSettingsModel : public QAbstractListModel DenyLocationAccess = 2 }; + enum NotificationsPreference { + AskForNotificationsAccess = 0, + AllowNotificationsAccess = 1, + DenyNotificationsAccess = 2 + }; + enum Roles { Domain = Qt::UserRole + 1, DomainWithoutSubdomain, AllowCustomUrlSchemes, AllowLocation, + AllowNotifications, UserAgentId, ZoomFactor }; @@ -68,15 +76,17 @@ class DomainSettingsModel : public QAbstractListModel Q_INVOKABLE bool contains(const QString& domain) const; Q_INVOKABLE void deleteAndResetDataBase(); Q_INVOKABLE bool areCustomUrlSchemesAllowed(const QString& domain); - Q_INVOKABLE void allowCustomUrlSchemes(const QString& domain, bool allow); + Q_INVOKABLE void allowCustomUrlSchemes(const QString& domain, bool allow, bool incognito); Q_INVOKABLE AllowLocationPreference getLocationPreference(const QString& domain) const; Q_INVOKABLE void setLocationPreference(const QString& domain, AllowLocationPreference preference); + Q_INVOKABLE NotificationsPreference getNotificationsPreference(const QString& domain) const; + Q_INVOKABLE void setNotificationsPreference(const QString& domain, NotificationsPreference, bool incognito); Q_INVOKABLE int getUserAgentId(const QString& domain) const; Q_INVOKABLE void setUserAgentId(const QString& domain, int userAgentId); Q_INVOKABLE void removeUserAgentIdFromAllDomains(int userAgentId); Q_INVOKABLE double getZoomFactor(const QString& domain) const; Q_INVOKABLE void setZoomFactor(const QString& domain, double zoomFactor); - Q_INVOKABLE void insertEntry(const QString& domain); + Q_INVOKABLE void insertEntry(const QString& domain, bool incognito); Q_INVOKABLE void removeEntry(const QString& domain); Q_SIGNALS: @@ -93,6 +103,7 @@ class DomainSettingsModel : public QAbstractListModel QString domainWithoutSubdomain; bool allowCustomUrlSchemes; AllowLocationPreference allowLocation; + NotificationsPreference allowNotifications; int userAgentId; double zoomFactor; }; diff --git a/src/app/notifications-proxy.cpp b/src/app/notifications-proxy.cpp new file mode 100644 index 000000000..7c9340750 --- /dev/null +++ b/src/app/notifications-proxy.cpp @@ -0,0 +1,76 @@ +/* + * Copyright 2020 UBports Foundation + * + * This file is part of morph-browser. + * + * morph-browser is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * morph-browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "notifications-proxy.h" +#include "pushclient/pushclient.h" + +#include +#include +#include + +NotificationsProxy::NotificationsProxy(QObject* parent) : QObject(parent) +{ + m_appId = QString(); + m_isWebApp = false; +} + +void NotificationsProxy::setAppId(const QString &appId) +{ + m_appId = appId; + PushClient::instance()->setAppId(m_appId); + m_isWebApp = ! m_appId.startsWith("_"); +} + +void NotificationsProxy::sendNotification(QObject * notificationObject) const +{ + QWebEngineNotification * notification = qobject_cast(notificationObject); + QJsonObject message = buildMessage(notification->tag(), notification->origin(), notification->title(), notification->message()); + // send notification, update it if tag does already exist + PushClient::instance()->update(notification->tag(), message); +} + +void NotificationsProxy::updateCount() const +{ + PushClient::instance()->updateCount(); +} + +QJsonObject NotificationsProxy::buildMessage(const QString & tag, const QUrl & origin, const QString & title, const QString & body) const +{ + QJsonObject notification; + notification["tag"] = tag; + notification["card"] = buildCard(origin, title, body); + notification["sound"] = false; + QJsonObject message; + message["notification"] = notification; + return message; +} + +QJsonObject NotificationsProxy::buildCard(const QUrl & origin, const QString & title, const QString & body) const +{ + QJsonObject card; + card["summary"] = title; + card["body"] = body; + card["popup"] = true; + card["persist"] = true; + + QJsonArray actions = QJsonArray(); + QString actionUri = m_isWebApp ? QString("appid://%1/%2/current-user-version").arg(m_appId.split("_").at(0), m_appId.split("_").at(1)) : origin.toString(); + actions.append(actionUri); + card["actions"] = actions; + return card; +} diff --git a/src/app/notifications-proxy.h b/src/app/notifications-proxy.h new file mode 100644 index 000000000..4819f2af1 --- /dev/null +++ b/src/app/notifications-proxy.h @@ -0,0 +1,44 @@ +/* + * Copyright 2020 UBports Foundation + * + * This file is part of morph-browser. + * + * morph-browser is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * morph-browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef __NOTIFICATIONS_PROXY_H__ +#define __NOTIFICATIONS_PROXY_H__ + +#include +#include + +class NotificationsProxy : public QObject +{ + Q_OBJECT + +public: + explicit NotificationsProxy(QObject* parent=0); + + Q_INVOKABLE void setAppId(const QString & appId); + Q_INVOKABLE void sendNotification(QObject * notificationObject) const; + Q_INVOKABLE void updateCount() const; + +private: + QJsonObject buildMessage(const QString & tag, const QUrl & origin, const QString & title, const QString & body) const; + QJsonObject buildCard(const QUrl & origin, const QString & title, const QString & body) const; + + QString m_appId; + bool m_isWebApp; +}; + +#endif // __NOTIFICATIONS_PROXY_H__ diff --git a/src/app/pushclient/pushclient.cpp b/src/app/pushclient/pushclient.cpp new file mode 100644 index 000000000..f60fd2ef7 --- /dev/null +++ b/src/app/pushclient/pushclient.cpp @@ -0,0 +1,189 @@ +/* Copyright (C) 2017 Dan Chapman (used pushclient.cpp of dekko as base) + * Copyright 2020 UBports Foundation + * + * This file is part of morph-browser + * + * morph-browser is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * morph-browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ +#include "pushclient.h" +#include +#include +#include +#include +#include + +#define POSTAL_SERVICE "com.ubuntu.Postal" +#define POSTAL_PATH "/com/ubuntu/Postal" +#define POSTAL_IFACE "com.ubuntu.Postal" + +static QPointer s_client; +PushClient *PushClient::instance() +{ + if (s_client.isNull()) { + s_client = new PushClient(); + } + return s_client; +} + +PushClient::PushClient(QObject *parent) : QObject(parent), + m_conn(QDBusConnection::sessionBus()) +{ +} + +void PushClient::setAppId(const QString & appId) +{ + m_appId = appId; + updateCount(); +} + +//shamelessly stolen from accounts-polld +bool PushClient::send(const QJsonObject &message) +{ + QDBusMessage msg = QDBusMessage::createMethodCall(POSTAL_SERVICE, + makePath(), + POSTAL_IFACE, + "Post"); + msg << m_appId; + QByteArray data = QJsonDocument(message).toJson(QJsonDocument::Compact); + msg << QString::fromUtf8(data); + + qDebug() << "[POST] >> " << msg; + + QDBusMessage reply = m_conn.call(msg); + if (reply.type() == QDBusMessage::ErrorMessage) { + qDebug() << "[POST ERROR] " << reply.errorMessage(); + return false; + } + qDebug() << "[POST SUCCESS] >> Message posted."; + QJsonObject n = message.value("notification").toObject(); + QString tag = n.value("tag").toString(); + updateCount(tag); + return true; +} + +bool PushClient::update(const QString &tag, const QJsonObject &message) +{ + if (hasTag(tag)) { + clearPersistent(tag); + } + return send(message); +} + +bool PushClient::hasTag(const QString &tag) +{ + return m_tags.contains(tag); +} + +bool PushClient::clearPersistent(const QString &tag) +{ + if (m_tags.contains(tag)) { + qDebug() << "[REMOVE] >> Removing message: " << tag; + QDBusMessage message = QDBusMessage::createMethodCall(POSTAL_SERVICE, + makePath(), + POSTAL_IFACE, + "ClearPersistent"); + message << m_appId; + message << tag; + + QDBusMessage reply = m_conn.call(message); + if (reply.type() == QDBusMessage::ErrorMessage) { + qDebug() << "[REMOVE ERROR] " << reply.errorMessage(); + return false; + } + qDebug() << "[REMOVE SUCCESS] Notification removed"; + return updateCount(tag, true); + } + return false; +} + +bool PushClient::updateCount(const QString &tag, const bool remove) +{ + qDebug() << "[COUNT] >> Updating launcher count"; + if (!tag.isEmpty()) { + if (!remove && !m_tags.contains(tag)) { + qDebug() << "[COUNT] >> Tag not yet in persistent list. adding it now: " << tag; + m_tags << tag; + } + + if (remove && m_tags.contains(tag)) { + qDebug() << "[COUNT] >> Removing tag from persistent list: " << tag; + m_tags.removeAll(tag); + } + } + else { + m_tags = getPersistent(); + } + + bool visible = m_tags.count() != 0; + QDBusMessage message = QDBusMessage::createMethodCall(POSTAL_SERVICE, + makePath(), + POSTAL_IFACE, + "SetCounter"); + message << m_appId << m_tags.count() << visible; + bool result = m_conn.send(message); + if (result) { + qDebug() << "[COUNT] >> Updated."; + } + return result; +} + +//shamelessly stolen from accounts-polld +QByteArray PushClient::makePath() +{ + QByteArray path(QByteArrayLiteral("/com/ubuntu/Postal/")); + + QByteArray pkg = m_appId.split('_').first().toUtf8(); + + // legacy apps have _ as path + if (pkg.count() == 0) { + path += '_'; + } + + // path for click apps + for (int i = 0; i < pkg.count(); i++) { + char buffer[10]; + char c = pkg[i]; + switch (c) { + case '+': + case '.': + case '-': + case ':': + case '~': + case '_': + sprintf(buffer, "_%.2x", c); + path += buffer; + break; + default: + path += c; + } + } + qDebug() << "[PATH] >> " << path; + return path; +} + +QStringList PushClient::getPersistent() +{ + QDBusMessage message = QDBusMessage::createMethodCall(POSTAL_SERVICE, + makePath(), + POSTAL_IFACE, + "ListPersistent"); + message << m_appId; + QDBusMessage reply = m_conn.call(message); + if (reply.type() == QDBusMessage::ErrorMessage) { + qDebug() << reply.errorMessage(); + return QStringList(); + } + QStringList tags = reply.arguments()[0].toStringList(); + qDebug() << "[TAGS] >> " << tags; + return tags; +} diff --git a/src/app/pushclient/pushclient.h b/src/app/pushclient/pushclient.h new file mode 100644 index 000000000..d4d68b462 --- /dev/null +++ b/src/app/pushclient/pushclient.h @@ -0,0 +1,54 @@ +/* Copyright (C) 2017 Dan Chapman (used pushclient.h of Dekko as base) + * Copyright 2020 UBports Foundation + * + * This file is part of morph-browser + * + * morph-browser is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * morph-browser is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ +#ifndef PUSHCLIENT_H +#define PUSHCLIENT_H + +#include +#include +#include + +class PushClient : public QObject +{ + Q_OBJECT +public: + explicit PushClient(QObject *parent = 0); + + static PushClient *instance(); + + // set the app id + void setAppId(const QString & appId); + + // Send a notification + bool send(const QJsonObject &message); + // Update a notification + bool update(const QString &tag, const QJsonObject &message); + + bool hasTag(const QString &tag); + bool clearPersistent(const QString &tag); + bool updateCount(const QString &tag = QString(), const bool remove = false); + +private: + QByteArray makePath(); + QStringList getPersistent(); + + QDBusConnection m_conn; + QStringList m_tags; + QString m_appId; +}; + +#endif // PUSHCLIENT_H diff --git a/src/app/webbrowser/Browser.qml b/src/app/webbrowser/Browser.qml index 7402b4424..81d6be797 100644 --- a/src/app/webbrowser/Browser.qml +++ b/src/app/webbrowser/Browser.qml @@ -63,6 +63,10 @@ Common.BrowserView { browser.bindExistingTab(tab); } + function findTabIndexWithUrl(url) { + return internal.findTabIndexWithUrl(url); + } + function moveTab(from, to) { if (from === to || from < 0 || from >= count @@ -185,7 +189,7 @@ Common.BrowserView { // handle custom schemes if (UrlUtils.hasCustomScheme(url)) { - if (! internal.areCustomUrlSchemesAllowed(currentDomain)) + if (! DomainSettingsModel.areCustomUrlSchemesAllowed(currentDomain)) { request.action = WebEngineNavigationRequest.IgnoreRequest; @@ -193,14 +197,14 @@ Common.BrowserView { allowCustomSchemesDialog.url = url; allowCustomSchemesDialog.domain = currentDomain; allowCustomSchemesDialog.showAllowPermanentlyCheckBox = ! browser.incognito; - allowCustomSchemesDialog.allow.connect(function() {internal.allowCustomUrlSchemes(currentDomain, false); - internal.navigateToUrlAsync(url); - } - ); - allowCustomSchemesDialog.allowPermanently.connect(function() {internal.allowCustomUrlSchemes(currentDomain, true); - internal.navigateToUrlAsync(url); - } - ); + allowCustomSchemesDialog.allow.connect(function() { + DomainSettingsModel.allowCustomUrlSchemes(currentDomain, true, true); + internal.navigateToUrlAsync(url); + }); + allowCustomSchemesDialog.allowPermanently.connect(function() { + DomainSettingsModel.allowCustomUrlSchemes(currentDomain, true, false); + internal.navigateToUrlAsync(url); + }); } return; } @@ -1432,6 +1436,15 @@ Common.BrowserView { } } + function findTabIndexWithUrl(url) { + var i; + for (i = 0; i < tabsModel.count; i++) { + if (tabsModel.get(i).url.toString() === url.toString()) + return i; + } + return -1; + } + function undoCloseTab() { if (!incognito && closedTabHistory.length > 0) { // For triggering property change on closedTabHistory @@ -1531,35 +1544,6 @@ Common.BrowserView { return false } - // domains the user has allowed custom protocols for this (incognito) session - property var domainsWithCustomUrlSchemesAllowed: [] - - function allowCustomUrlSchemes(domain, allowPermanently) { - domainsWithCustomUrlSchemesAllowed.push(domain); - - if (allowPermanently) - { - DomainSettingsModel.allowCustomUrlSchemes(domain, true); - } - } - - function areCustomUrlSchemesAllowed(domain) { - - for (var i in domainsWithCustomUrlSchemesAllowed) { - if (domain === domainsWithCustomUrlSchemesAllowed[i]) { - return true; - } - } - - if (DomainSettingsModel.areCustomUrlSchemesAllowed(domain)) - { - domainsWithCustomUrlSchemesAllowed.push(domain); - return true; - } - - return false; - } - function historyGoBack() { if (currentWebview && currentWebview.canGoBack) { internal.resetFocus() @@ -1970,6 +1954,11 @@ Common.BrowserView { internal.closeTabsWithUrl(download.url); } } + + onPresentNotification: { + NotificationsProxy.updateCount(); + NotificationsProxy.sendNotification(notification); + } } Connections { diff --git a/src/app/webbrowser/morph-browser.qml b/src/app/webbrowser/morph-browser.qml index 119aae939..db500c91e 100644 --- a/src/app/webbrowser/morph-browser.qml +++ b/src/app/webbrowser/morph-browser.qml @@ -61,6 +61,12 @@ QtObject { // create path for pages printed to PDF FileOperations.mkpath(Qt.resolvedUrl(cacheLocation) + "/pdf_tmp"); + + // set appId of NotificationsProxy + NotificationsProxy.setAppId("_morph-browser"); + + // update notification count + NotificationsProxy.updateCount(); } // Array of all windows, sorted chronologically (most recently active last) @@ -79,17 +85,25 @@ QtObject { function openUrls(urls, newWindow, incognito) { var window = getLastActiveWindow(incognito) if (!window || newWindow) { - window = windowFactory.createObject(null, {"incognito": incognito}) + window = windowFactory.createObject(null, {"incognito": incognito}); } for (var i in urls) { - window.addTab(urls[i]).load() + var tabIndexWithUrl = window.tabsModel.findTabIndexWithUrl(urls[i]); + + if (tabIndexWithUrl >= 0) { + window.tabsModel.selectTab(tabIndexWithUrl); + } + else { + window.addTab(urls[i]).load(); + window.tabsModel.currentIndex = window.tabsModel.count - 1; + } } if (window.tabsModel.count === 0) { - window.addTab().load() + window.addTab().load(); + window.tabsModel.currentIndex = window.tabsModel.count - 1; } - window.tabsModel.currentIndex = window.tabsModel.count - 1 - window.show() - window.requestActivate() + window.show(); + window.requestActivate(); } property var windowFactory: Component { @@ -115,55 +129,56 @@ QtObject { onActiveChanged: { if (active) { - var index = allWindows.indexOf(this) + var index = allWindows.indexOf(this); if (index > -1) { - allWindows.push(allWindows.splice(index, 1)[0]) + allWindows.push(allWindows.splice(index, 1)[0]); } + NotificationsProxy.updateCount(); } } onClosing: { if (allWindows.length == 1) { if (tabsModel.count > 0) { - session.save() + session.save(); } else { - session.clear() + session.clear(); } } if (incognito && (allWindows.length > 1)) { // If the last incognito window is being closed, // prune incognito entries from the downloads model - var incognitoWindows = 0 + var incognitoWindows = 0; for (var w in allWindows) { - var window = allWindows[w] + var window = allWindows[w]; if ((window !== this) && window.incognito) { - ++incognitoWindows + ++incognitoWindows; } } - if (incognitoWindows == 0) { - DownloadsModel.pruneIncognitoDownloads() + if (incognitoWindows === 0) { + DownloadsModel.pruneIncognitoDownloads(); } } - - if (allWindows.length > 1) { for (var win in allWindows) { if (this === allWindows[win]) { - var tabs = allWindows[win].tabsModel + var tabs = allWindows[win].tabsModel; for (var t = tabs.count - 1; t >= 0; --t) { - //console.log("remove tab with url " + tabs.get(t).url) - tabs.removeTab(t) + //console.log("remove tab with url " + tabs.get(t).url); + tabs.removeTab(t); } - allWindows.splice(win, 1) - return + allWindows.splice(win, 1); + return; } } } - destroy() + NotificationsProxy.updateCount(); + + destroy(); } Shortcut { diff --git a/src/app/webcontainer/WebApp.qml b/src/app/webcontainer/WebApp.qml index 3235af014..bc4b25c56 100644 --- a/src/app/webcontainer/WebApp.qml +++ b/src/app/webcontainer/WebApp.qml @@ -512,6 +512,11 @@ Common.BrowserView { console.log("a download was finished with path %1.".arg(download.path)) webapp.setDownloadComplete(download) } + + onPresentNotification: { + NotificationsProxy.updateCount(); + NotificationsProxy.sendNotification(notification); + } } Connections { diff --git a/src/app/webcontainer/WebViewImplOxide.qml b/src/app/webcontainer/WebViewImplOxide.qml index ba96ef33e..f17aa4c3b 100644 --- a/src/app/webcontainer/WebViewImplOxide.qml +++ b/src/app/webcontainer/WebViewImplOxide.qml @@ -215,35 +215,6 @@ WebappWebview { currentWebview.runJavaScript("window.location.href = '%1';".arg(targetUrl)); } - // domains the user has allowed custom protocols for this (incognito) session - property var domainsWithCustomUrlSchemesAllowed: [] - - function allowCustomUrlSchemes(domain, allowPermanently) { - domainsWithCustomUrlSchemesAllowed.push(domain); - - if (allowPermanently) - { - DomainSettingsModel.allowCustomUrlSchemes(domain, true); - } - } - - function areCustomUrlSchemesAllowed(domain) { - - for (var i in domainsWithCustomUrlSchemesAllowed) { - if (domain === domainsWithCustomUrlSchemesAllowed[i]) { - return true; - } - } - - if (DomainSettingsModel.areCustomUrlSchemesAllowed(domain)) - { - domainsWithCustomUrlSchemesAllowed.push(domain); - return true; - } - - return false; - } - function navigationRequestedDelegate(request) { var url = request.url.toString(); @@ -255,7 +226,7 @@ WebappWebview { // handle custom schemes if (UrlUtils.hasCustomScheme(url)) { - if (! areCustomUrlSchemesAllowed(currentDomain)) + if (! DomainSettingsModel.areCustomUrlSchemesAllowed(currentDomain)) { request.action = WebEngineNavigationRequest.IgnoreRequest; @@ -263,14 +234,14 @@ WebappWebview { allowCustomSchemesDialog.url = url; allowCustomSchemesDialog.domain = currentDomain; allowCustomSchemesDialog.showAllowPermanentlyCheckBox = true; - allowCustomSchemesDialog.allow.connect(function() {allowCustomUrlSchemes(currentDomain, false); - navigateToUrlAsync(url); - } - ); - allowCustomSchemesDialog.allowPermanently.connect(function() {allowCustomUrlSchemes(currentDomain, true); - navigateToUrlAsync(url); - } - ); + allowCustomSchemesDialog.allow.connect(function() { + DomainSettingsModel.allowCustomUrlSchemes(currentDomain, true, true); + navigateToUrlAsync(url); + }); + allowCustomSchemesDialog.allowPermanently.connect(function() { + DomainSettingsModel.allowCustomUrlSchemes(currentDomain, true, false); + navigateToUrlAsync(url); + }); } return; } diff --git a/src/app/webcontainer/webapp-container.qml b/src/app/webcontainer/webapp-container.qml index 949a3c815..4e55b579a 100644 --- a/src/app/webcontainer/webapp-container.qml +++ b/src/app/webcontainer/webapp-container.qml @@ -63,6 +63,16 @@ BrowserWindow { // Used for testing signal schemeUriHandleFilterResult(string uri) + onActiveChanged: { + if (active) { + NotificationsProxy.updateCount(); + } + } + + onClosing: { + NotificationsProxy.updateCount(); + } + function getWindowTitle() { var webappViewTitle = webappViewLoader.item @@ -187,6 +197,12 @@ BrowserWindow { // create path for pages printed to PDF FileOperations.mkpath(Qt.resolvedUrl(cacheLocation) + "/pdf_tmp"); + + // set appId for NotificationsProxy + NotificationsProxy.setAppId(unversionedAppId); + + // update notifications count + NotificationsProxy.updateCount(); } function loadCustomUserScripts() {