feat: Add URL trust options when the user clicks on a link

- add a popup asking the user before clicking on an unfurled link
preview
- add a checkbox for the above popup to remember the trust for such
domain
- use local Settings to persist the "trust domain" locally; for
global persistence across devices, see
https://github.com/status-im/status-go/issues/4132

Closes #12388
This commit is contained in:
Lukáš Tinkl 2023-10-10 16:23:48 +02:00 committed by Lukáš Tinkl
parent 56547c0ae1
commit 2abe0358fc
12 changed files with 148 additions and 10 deletions

View File

@ -55,7 +55,7 @@ QtObject:
return $(response.result)
except Exception as e:
error "error: ", procName="removeReaction", errName = e.name, errDesription = e.msg
error "error: ", procName="getLinkPreviewWhitelist", errName = e.name, errDesription = e.msg
proc getDefaultAccount(self: Service): string =
try:

View File

@ -13,10 +13,12 @@ class StringUtilsInternal : public QObject
public:
explicit StringUtilsInternal(QQmlEngine* engine, QObject* parent = nullptr);
Q_INVOKABLE QString escapeHtml(const QString &unsafe) const;
Q_INVOKABLE QString escapeHtml(const QString& unsafe) const;
Q_INVOKABLE QString readTextFile(const QString& filePath) const;
Q_INVOKABLE QString extractDomainFromLink(const QString& link) const;
private:
QQmlEngine *m_engine{nullptr};
QQmlEngine* m_engine{nullptr};
};

View File

@ -85,6 +85,6 @@ CheckBox {
HoverHandler {
acceptedDevices: PointerDevice.Mouse
cursorShape: Qt.PointingHandCursor
cursorShape: root.changeCursor ? Qt.PointingHandCursor : undefined
}
}

View File

@ -12,4 +12,8 @@ QtObject {
function readTextFile(file) {
return Internal.StringUtils.readTextFile(file)
}
function extractDomainFromLink(link) {
return Internal.StringUtils.extractDomainFromLink(link)
}
}

View File

@ -4,6 +4,7 @@
#include <QFileSelector>
#include <QQmlEngine>
#include <QQmlFileSelector>
#include <QUrl>
StringUtilsInternal::StringUtilsInternal(QQmlEngine* engine, QObject* parent)
: m_engine(engine)
@ -33,3 +34,13 @@ QString StringUtilsInternal::readTextFile(const QString& filePath) const
return file.readAll();
}
QString StringUtilsInternal::extractDomainFromLink(const QString& link) const
{
const auto url = QUrl::fromUserInput(link);
if (!url.isValid()) {
qWarning() << Q_FUNC_INFO << "Invalid URL:" << link;
return {};
}
return url.host();
}

View File

@ -3,6 +3,7 @@ import QtQuick.Controls 2.13
import QtQuick.Layouts 1.13
import QtMultimedia 5.13
import Qt.labs.platform 1.1
import Qt.labs.settings 1.1
import QtQml.Models 2.14
import QtQml 2.15
@ -215,6 +216,11 @@ Item {
}
}
Settings {
id: appMainLocalSettings
property var whitelistedUnfurledDomains: []
}
Popups {
id: popups
popupParent: appMain
@ -222,6 +228,15 @@ Item {
communitiesStore: appMain.communitiesStore
devicesStore: appMain.rootStore.profileSectionStore.devicesStore
isDevBuild: !production
onOpenExternalLink: globalConns.onOpenLink(link)
onSaveDomainToUnfurledWhitelist: {
const whitelistedHostnames = appMainLocalSettings.whitelistedUnfurledDomains || []
if (!whitelistedHostnames.includes(domain)) {
whitelistedHostnames.push(domain)
appMainLocalSettings.whitelistedUnfurledDomains = whitelistedHostnames
}
}
}
Connections {
@ -250,8 +265,6 @@ Item {
}
function onOpenLink(link: string) {
console.warn("opening external url without asking user")
// Qt sometimes inserts random HTML tags; and this will break on invalid URL inside QDesktopServices::openUrl(link)
link = appMain.rootStore.plainText(link)
@ -267,6 +280,13 @@ Item {
}
}
function onOpenLinkWithConfirmation(link: string, domain: string) {
if (appMainLocalSettings.whitelistedUnfurledDomains.includes(domain) || !!localAccountSensitiveSettings.whitelistedUnfurlingSites[domain])
globalConns.onOpenLink(link)
else
popups.openConfirmExternalLinkPopup(link, domain)
}
function onPlaySendMessageSound() {
sendMessageSound.stop()
sendMessageSound.play()
@ -1618,7 +1638,7 @@ Item {
settingsUpdated = true
}
whitelistedHostnames.push(site.address)
})
})
// Remove any whitelisted sites from app settings that don't exist in the
// whitelist from status-go
Object.keys(settings).forEach(settingsHostname => {

View File

@ -31,6 +31,9 @@ QtObject {
property var devicesStore
property bool isDevBuild
signal openExternalLink(string link)
signal saveDomainToUnfurledWhitelist(string domain)
property var activePopupComponents: []
Component.onCompleted: {
@ -290,6 +293,10 @@ QtObject {
openPopup(transferOwnershipPopup, { communityName, communityLogo, token, accounts, sendModalPopup })
}
function openConfirmExternalLinkPopup(link, domain) {
openPopup(confirmExternalLinkPopup, {link, domain})
}
readonly property list<Component> _components: [
Component {
id: removeContactConfirmationDialog
@ -751,6 +758,15 @@ QtObject {
TransferOwnershipPopup {
onClosed: destroy()
}
},
Component {
id: confirmExternalLinkPopup
ConfirmExternalLinkPopup {
destroyOnClose: true
onOpenExternalLink: root.openExternalLink(link)
onSaveDomainToUnfurledWhitelist: root.saveDomainToUnfurledWhitelist(domain)
}
}
]
}

View File

@ -0,0 +1,85 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import utils 1.0
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Core 0.1
StatusDialog {
id: root
required property string link
required property string domain
signal openExternalLink(string link)
signal saveDomainToUnfurledWhitelist(string domain)
width: 521
header: StatusDialogHeader {
headline.title: qsTr("Before you go")
actions.closeButton.onClicked: root.close()
leftComponent: StatusRoundIcon {
asset.name: "browser"
asset.isImage: true
}
}
contentItem: ColumnLayout {
spacing: 20
StatusBaseText {
Layout.fillWidth: true
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
text: qsTr("This link is taking you to the following site. Be careful to double check the URL before you go.")
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 66
radius: Style.current.halfPadding
color: Theme.palette.baseColor4
StatusBaseText {
anchors.fill: parent
anchors.leftMargin: Style.current.padding
anchors.rightMargin: Style.current.padding
anchors.topMargin: 11
anchors.bottomMargin: Style.current.halfPadding
text: root.link
wrapMode: Text.WrapAnywhere
elide: Text.ElideRight
}
}
StatusCheckBox {
id: trustDomainCheckbox
Layout.fillWidth: true
text: qsTr("Trust <b>%1</b> links from now on").arg(root.domain)
}
}
footer: StatusDialogFooter {
rightButtons: ObjectModel {
StatusFlatButton {
text: qsTr("Cancel")
onClicked: root.close()
}
StatusButton {
text: qsTr("Visit site")
onClicked: {
// (optionally) save the domain to whitelist
if (trustDomainCheckbox.checked) {
root.saveDomainToUnfurledWhitelist(root.domain)
}
root.openExternalLink(root.link)
root.close()
}
}
}
}
}

View File

@ -25,3 +25,4 @@ RenameGroupPopup 1.0 RenameGroupPopup.qml
DeleteMessageConfirmationPopup 1.0 DeleteMessageConfirmationPopup.qml
UserAgreementPopup 1.0 UserAgreementPopup.qml
AlertPopup 1.0 AlertPopup.qml
ConfirmExternalLinkPopup 1.0 ConfirmExternalLinkPopup.qml

View File

@ -116,7 +116,7 @@ Flow {
root.imageClicked(unfurledLink, mouse, "", url) // request a dumb context menu with just "copy/open link" items
break
default:
Global.openLink(url) // FIXME https://github.com/status-im/status-desktop/issues/12388
Global.openLinkWithConfirmation(url, hostname)
break
}
}

View File

@ -73,7 +73,6 @@ Loader {
const arr = links.split(separator)
const filtered = arr.filter(v => v.toLowerCase().endsWith('.gif') || v.toLowerCase().startsWith(Constants.userLinkPrefix.toLowerCase()))
const out = filtered.join(separator)
console.log(`<<<${arr}->${out}`)
return out
}
@ -756,7 +755,6 @@ Loader {
linksComponent: Component {
LinksMessageView {
id: linksMessageView
linkPreviewModel: root.linkPreviewModel
localUnfurlLinks: root.localUnfurlLinks
messageStore: root.messageStore

View File

@ -56,6 +56,7 @@ QtObject {
var sendModalPopup)
signal openLink(string link)
signal openLinkWithConfirmation(string link, string domain)
signal setNthEnabledSectionActive(int nthSection)
signal appSectionBySectionTypeChanged(int sectionType, int subsection)