mirror of
https://github.com/lubosz/overte.git
synced 2025-08-07 20:31:29 +02:00
Merge pull request #8828 from birarda/web-engine-interceptor
move HF access token to authorization header
This commit is contained in:
commit
9f2627d31d
15 changed files with 145 additions and 82 deletions
|
@ -41,8 +41,10 @@ endif ()
|
||||||
|
|
||||||
if (ANDROID)
|
if (ANDROID)
|
||||||
set(PLATFORM_QT_COMPONENTS AndroidExtras)
|
set(PLATFORM_QT_COMPONENTS AndroidExtras)
|
||||||
|
set(PLATFORM_QT_LIBRARIES Qt5::AndroidExtras)
|
||||||
else ()
|
else ()
|
||||||
set(PLATFORM_QT_COMPONENTS WebEngine WebEngineWidgets)
|
set(PLATFORM_QT_COMPONENTS WebEngine WebEngineWidgets)
|
||||||
|
set(PLATFORM_QT_LIBRARIES Qt5::WebEngine Qt5::WebEngineWidgets)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
find_package(
|
find_package(
|
||||||
|
@ -244,7 +246,8 @@ target_link_libraries(
|
||||||
${TARGET_NAME}
|
${TARGET_NAME}
|
||||||
Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL
|
Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL
|
||||||
Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg
|
Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg
|
||||||
Qt5::WebChannel Qt5::WebEngine
|
Qt5::WebChannel Qt5::WebEngine
|
||||||
|
${PLATFORM_QT_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (UNIX)
|
if (UNIX)
|
||||||
|
|
|
@ -10,10 +10,10 @@
|
||||||
|
|
||||||
import QtQuick 2.5
|
import QtQuick 2.5
|
||||||
import QtWebEngine 1.2
|
import QtWebEngine 1.2
|
||||||
|
import HFWebEngineProfile 1.0
|
||||||
|
|
||||||
WebEngineView {
|
WebEngineView {
|
||||||
id: root
|
id: root
|
||||||
property var newUrl;
|
|
||||||
|
|
||||||
profile: desktop.browserProfile
|
profile: desktop.browserProfile
|
||||||
|
|
||||||
|
@ -25,28 +25,6 @@ WebEngineView {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME hack to get the URL with the auth token included. Remove when we move to Qt 5.6
|
|
||||||
Timer {
|
|
||||||
id: urlReplacementTimer
|
|
||||||
running: false
|
|
||||||
repeat: false
|
|
||||||
interval: 50
|
|
||||||
onTriggered: url = newUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
onUrlChanged: {
|
|
||||||
var originalUrl = url.toString();
|
|
||||||
newUrl = urlHandler.fixupUrl(originalUrl).toString();
|
|
||||||
if (newUrl !== originalUrl) {
|
|
||||||
root.stop();
|
|
||||||
if (urlReplacementTimer.running) {
|
|
||||||
console.warn("Replacement timer already running");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
urlReplacementTimer.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoadingChanged: {
|
onLoadingChanged: {
|
||||||
// Required to support clicking on "hifi://" links
|
// Required to support clicking on "hifi://" links
|
||||||
if (WebEngineView.LoadStartedStatus == loadRequest.status) {
|
if (WebEngineView.LoadStartedStatus == loadRequest.status) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import QtQuick 2.5
|
||||||
import QtWebEngine 1.1
|
import QtWebEngine 1.1
|
||||||
import QtWebChannel 1.0
|
import QtWebChannel 1.0
|
||||||
import "../controls-uit" as HiFiControls
|
import "../controls-uit" as HiFiControls
|
||||||
|
import HFWebEngineProfile 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
property alias url: root.url
|
property alias url: root.url
|
||||||
|
@ -31,6 +32,11 @@ Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height
|
height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height
|
||||||
|
|
||||||
|
profile: HFWebEngineProfile {
|
||||||
|
id: webviewProfile
|
||||||
|
storageName: "qmlWebEngine"
|
||||||
|
}
|
||||||
|
|
||||||
// creates a global EventBridge object.
|
// creates a global EventBridge object.
|
||||||
WebEngineScript {
|
WebEngineScript {
|
||||||
id: createGlobalEventBridge
|
id: createGlobalEventBridge
|
||||||
|
@ -62,28 +68,6 @@ Item {
|
||||||
root.profile.httpUserAgent = "Mozilla/5.0 Chrome (HighFidelityInterface)";
|
root.profile.httpUserAgent = "Mozilla/5.0 Chrome (HighFidelityInterface)";
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME hack to get the URL with the auth token included. Remove when we move to Qt 5.6
|
|
||||||
Timer {
|
|
||||||
id: urlReplacementTimer
|
|
||||||
running: false
|
|
||||||
repeat: false
|
|
||||||
interval: 50
|
|
||||||
onTriggered: url = root.newUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
onUrlChanged: {
|
|
||||||
var originalUrl = url.toString();
|
|
||||||
root.newUrl = urlHandler.fixupUrl(originalUrl).toString();
|
|
||||||
if (root.newUrl !== originalUrl) {
|
|
||||||
root.stop();
|
|
||||||
if (urlReplacementTimer.running) {
|
|
||||||
console.warn("Replacement timer already running");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
urlReplacementTimer.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onFeaturePermissionRequested: {
|
onFeaturePermissionRequested: {
|
||||||
grantFeaturePermission(securityOrigin, feature, true);
|
grantFeaturePermission(securityOrigin, feature, true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ FocusScope {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onHeightChanged: d.handleSizeChanged();
|
onHeightChanged: d.handleSizeChanged();
|
||||||
|
|
||||||
onWidthChanged: d.handleSizeChanged();
|
onWidthChanged: d.handleSizeChanged();
|
||||||
|
|
|
@ -2,6 +2,7 @@ import QtQuick 2.5
|
||||||
import QtQuick.Controls 1.4
|
import QtQuick.Controls 1.4
|
||||||
import QtWebEngine 1.1;
|
import QtWebEngine 1.1;
|
||||||
import Qt.labs.settings 1.0
|
import Qt.labs.settings 1.0
|
||||||
|
import HFWebEngineProfile 1.0
|
||||||
|
|
||||||
import "../desktop" as OriginalDesktop
|
import "../desktop" as OriginalDesktop
|
||||||
import ".."
|
import ".."
|
||||||
|
@ -20,17 +21,14 @@ OriginalDesktop.Desktop {
|
||||||
onEntered: ApplicationCompositor.reticleOverDesktop = true
|
onEntered: ApplicationCompositor.reticleOverDesktop = true
|
||||||
onExited: ApplicationCompositor.reticleOverDesktop = false
|
onExited: ApplicationCompositor.reticleOverDesktop = false
|
||||||
acceptedButtons: Qt.NoButton
|
acceptedButtons: Qt.NoButton
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The tool window, one instance
|
// The tool window, one instance
|
||||||
property alias toolWindow: toolWindow
|
property alias toolWindow: toolWindow
|
||||||
ToolWindow { id: toolWindow }
|
ToolWindow { id: toolWindow }
|
||||||
|
|
||||||
property var browserProfile: WebEngineProfile {
|
property var browserProfile: HFWebEngineProfile {
|
||||||
id: webviewProfile
|
id: webviewProfile
|
||||||
httpUserAgent: "Chrome/48.0 (HighFidelityInterface)"
|
|
||||||
storageName: "qmlWebEngine"
|
storageName: "qmlWebEngine"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,5 +125,3 @@ OriginalDesktop.Desktop {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,8 @@
|
||||||
#include <QtQml/QQmlEngine>
|
#include <QtQml/QQmlEngine>
|
||||||
#include <QtQuick/QQuickWindow>
|
#include <QtQuick/QQuickWindow>
|
||||||
|
|
||||||
|
#include <QtWebEngineWidgets/QWebEngineProfile>
|
||||||
|
|
||||||
#include <QtWidgets/QDesktopWidget>
|
#include <QtWidgets/QDesktopWidget>
|
||||||
#include <QtWidgets/QMessageBox>
|
#include <QtWidgets/QMessageBox>
|
||||||
|
|
||||||
|
@ -65,6 +67,7 @@
|
||||||
#include <gpu/gl/GLBackend.h>
|
#include <gpu/gl/GLBackend.h>
|
||||||
#include <HFActionEvent.h>
|
#include <HFActionEvent.h>
|
||||||
#include <HFBackEvent.h>
|
#include <HFBackEvent.h>
|
||||||
|
#include <HFWebEngineProfile.h>
|
||||||
#include <InfoView.h>
|
#include <InfoView.h>
|
||||||
#include <input-plugins/InputPlugin.h>
|
#include <input-plugins/InputPlugin.h>
|
||||||
#include <controllers/UserInputMapper.h>
|
#include <controllers/UserInputMapper.h>
|
||||||
|
@ -126,6 +129,7 @@
|
||||||
#include "InterfaceLogging.h"
|
#include "InterfaceLogging.h"
|
||||||
#include "LODManager.h"
|
#include "LODManager.h"
|
||||||
#include "ModelPackager.h"
|
#include "ModelPackager.h"
|
||||||
|
|
||||||
#include "scripting/AccountScriptingInterface.h"
|
#include "scripting/AccountScriptingInterface.h"
|
||||||
#include "scripting/AssetMappingsScriptingInterface.h"
|
#include "scripting/AssetMappingsScriptingInterface.h"
|
||||||
#include "scripting/AudioDeviceScriptingInterface.h"
|
#include "scripting/AudioDeviceScriptingInterface.h"
|
||||||
|
@ -1698,6 +1702,7 @@ void Application::initializeUi() {
|
||||||
UpdateDialog::registerType();
|
UpdateDialog::registerType();
|
||||||
qmlRegisterType<Preference>("Hifi", 1, 0, "Preference");
|
qmlRegisterType<Preference>("Hifi", 1, 0, "Preference");
|
||||||
|
|
||||||
|
qmlRegisterType<HFWebEngineProfile>("HFWebEngineProfile", 1, 0, "HFWebEngineProfile");
|
||||||
|
|
||||||
auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
||||||
offscreenUi->create(_glWidget->qglContext());
|
offscreenUi->create(_glWidget->qglContext());
|
||||||
|
|
|
@ -40,19 +40,6 @@
|
||||||
#include "TextureRecycler.h"
|
#include "TextureRecycler.h"
|
||||||
#include "Context.h"
|
#include "Context.h"
|
||||||
|
|
||||||
QString fixupHifiUrl(const QString& urlString) {
|
|
||||||
static const QString ACCESS_TOKEN_PARAMETER = "access_token";
|
|
||||||
static const QString ALLOWED_HOST = "metaverse.highfidelity.com";
|
|
||||||
QUrl url(urlString);
|
|
||||||
QUrlQuery query(url);
|
|
||||||
if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) {
|
|
||||||
auto accountManager = DependencyManager::get<AccountManager>();
|
|
||||||
query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager->getAccountInfo().getAccessToken().token);
|
|
||||||
url.setQuery(query.query());
|
|
||||||
return url.toString();
|
|
||||||
}
|
|
||||||
return urlString;
|
|
||||||
}
|
|
||||||
|
|
||||||
class UrlHandler : public QObject {
|
class UrlHandler : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -66,11 +53,6 @@ public:
|
||||||
static auto handler = dynamic_cast<AbstractUriHandler*>(qApp);
|
static auto handler = dynamic_cast<AbstractUriHandler*>(qApp);
|
||||||
return handler->acceptURL(url);
|
return handler->acceptURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME hack for authentication, remove when we migrate to Qt 5.6
|
|
||||||
Q_INVOKABLE QString fixupUrl(const QString& originalUrl) {
|
|
||||||
return fixupHifiUrl(originalUrl);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Time between receiving a request to render the offscreen UI actually triggering
|
// Time between receiving a request to render the offscreen UI actually triggering
|
||||||
|
|
|
@ -143,13 +143,13 @@ void XMLHttpRequestClass::open(const QString& method, const QString& url, bool a
|
||||||
if (url.toLower().left(METAVERSE_API_URL.length()) == METAVERSE_API_URL) {
|
if (url.toLower().left(METAVERSE_API_URL.length()) == METAVERSE_API_URL) {
|
||||||
auto accountManager = DependencyManager::get<AccountManager>();
|
auto accountManager = DependencyManager::get<AccountManager>();
|
||||||
|
|
||||||
if (accountManager->hasValidAccessToken()) {
|
if (_url.scheme() == "https" && accountManager->hasValidAccessToken()) {
|
||||||
QUrlQuery urlQuery(_url.query());
|
static const QString HTTP_AUTHORIZATION_HEADER = "Authorization";
|
||||||
urlQuery.addQueryItem("access_token", accountManager->getAccountInfo().getAccessToken().token);
|
QString bearerString = "Bearer " + accountManager->getAccountInfo().getAccessToken().token;
|
||||||
_url.setQuery(urlQuery);
|
_request.setRawHeader(HTTP_AUTHORIZATION_HEADER.toLocal8Bit(), bearerString.toLocal8Bit());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!username.isEmpty()) {
|
if (!username.isEmpty()) {
|
||||||
_url.setUserName(username);
|
_url.setUserName(username);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
set(TARGET_NAME ui)
|
set(TARGET_NAME ui)
|
||||||
setup_hifi_library(OpenGL Network Qml Quick Script WebChannel WebSockets XmlPatterns)
|
setup_hifi_library(OpenGL Network Qml Quick Script WebChannel WebEngine WebSockets XmlPatterns)
|
||||||
link_hifi_libraries(shared networking gl)
|
link_hifi_libraries(shared networking gl)
|
||||||
|
|
27
libraries/ui/src/HFWebEngineProfile.cpp
Normal file
27
libraries/ui/src/HFWebEngineProfile.cpp
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
//
|
||||||
|
// HFWebEngineProfile.cpp
|
||||||
|
// interface/src/networking
|
||||||
|
//
|
||||||
|
// Created by Stephen Birarda on 2016-10-17.
|
||||||
|
// Copyright 2016 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "HFWebEngineProfile.h"
|
||||||
|
|
||||||
|
#include "HFWebEngineRequestInterceptor.h"
|
||||||
|
|
||||||
|
static const QString QML_WEB_ENGINE_STORAGE_NAME = "qmlWebEngine";
|
||||||
|
|
||||||
|
HFWebEngineProfile::HFWebEngineProfile(QObject* parent) :
|
||||||
|
QQuickWebEngineProfile(parent)
|
||||||
|
{
|
||||||
|
static const QString WEB_ENGINE_USER_AGENT = "Chrome/48.0 (HighFidelityInterface)";
|
||||||
|
setHttpUserAgent(WEB_ENGINE_USER_AGENT);
|
||||||
|
|
||||||
|
// we use the HFWebEngineRequestInterceptor to make sure that web requests are authenticated for the interface user
|
||||||
|
auto requestInterceptor = new HFWebEngineRequestInterceptor(this);
|
||||||
|
setRequestInterceptor(requestInterceptor);
|
||||||
|
}
|
25
libraries/ui/src/HFWebEngineProfile.h
Normal file
25
libraries/ui/src/HFWebEngineProfile.h
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
//
|
||||||
|
// HFWebEngineProfile.h
|
||||||
|
// interface/src/networking
|
||||||
|
//
|
||||||
|
// Created by Stephen Birarda on 2016-10-17.
|
||||||
|
// Copyright 2016 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifndef hifi_HFWebEngineProfile_h
|
||||||
|
#define hifi_HFWebEngineProfile_h
|
||||||
|
|
||||||
|
#include <QtWebEngine/QQuickWebEngineProfile>
|
||||||
|
|
||||||
|
class HFWebEngineProfile : public QQuickWebEngineProfile {
|
||||||
|
public:
|
||||||
|
HFWebEngineProfile(QObject* parent = Q_NULLPTR);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
#endif // hifi_HFWebEngineProfile_h
|
40
libraries/ui/src/HFWebEngineRequestInterceptor.cpp
Normal file
40
libraries/ui/src/HFWebEngineRequestInterceptor.cpp
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
//
|
||||||
|
// HFWebEngineRequestInterceptor.cpp
|
||||||
|
// interface/src/networking
|
||||||
|
//
|
||||||
|
// Created by Stephen Birarda on 2016-10-14.
|
||||||
|
// Copyright 2016 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "HFWebEngineRequestInterceptor.h"
|
||||||
|
|
||||||
|
#include <QtCore/QDebug>
|
||||||
|
|
||||||
|
#include <AccountManager.h>
|
||||||
|
|
||||||
|
bool isAuthableHighFidelityURL(const QUrl& url) {
|
||||||
|
static const QStringList HF_HOSTS = {
|
||||||
|
"highfidelity.com", "highfidelity.io",
|
||||||
|
"metaverse.highfidelity.com", "metaverse.highfidelity.io"
|
||||||
|
};
|
||||||
|
|
||||||
|
return url.scheme() == "https" && HF_HOSTS.contains(url.host());
|
||||||
|
}
|
||||||
|
|
||||||
|
void HFWebEngineRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info) {
|
||||||
|
// check if this is a request to a highfidelity URL
|
||||||
|
if (isAuthableHighFidelityURL(info.requestUrl())) {
|
||||||
|
// if we have an access token, add it to the right HTTP header for authorization
|
||||||
|
auto accountManager = DependencyManager::get<AccountManager>();
|
||||||
|
|
||||||
|
if (accountManager->hasValidAccessToken()) {
|
||||||
|
static const QString OAUTH_AUTHORIZATION_HEADER = "Authorization";
|
||||||
|
|
||||||
|
QString bearerTokenString = "Bearer " + accountManager->getAccountInfo().getAccessToken().token;
|
||||||
|
info.setHttpHeader(OAUTH_AUTHORIZATION_HEADER.toLocal8Bit(), bearerTokenString.toLocal8Bit());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
libraries/ui/src/HFWebEngineRequestInterceptor.h
Normal file
26
libraries/ui/src/HFWebEngineRequestInterceptor.h
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
//
|
||||||
|
// HFWebEngineRequestInterceptor.h
|
||||||
|
// interface/src/networking
|
||||||
|
//
|
||||||
|
// Created by Stephen Birarda on 2016-10-14.
|
||||||
|
// Copyright 2016 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifndef hifi_HFWebEngineRequestInterceptor_h
|
||||||
|
#define hifi_HFWebEngineRequestInterceptor_h
|
||||||
|
|
||||||
|
#include <QWebEngineUrlRequestInterceptor>
|
||||||
|
|
||||||
|
class HFWebEngineRequestInterceptor : public QWebEngineUrlRequestInterceptor {
|
||||||
|
public:
|
||||||
|
HFWebEngineRequestInterceptor(QObject* parent) : QWebEngineUrlRequestInterceptor(parent) {};
|
||||||
|
|
||||||
|
virtual void interceptRequest(QWebEngineUrlRequestInfo& info) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // hifi_HFWebEngineRequestInterceptor_h
|
|
@ -86,13 +86,10 @@ QString QmlWebWindowClass::getURL() const {
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// HACK find a good place to declare and store this
|
|
||||||
extern QString fixupHifiUrl(const QString& urlString);
|
|
||||||
|
|
||||||
void QmlWebWindowClass::setURL(const QString& urlString) {
|
void QmlWebWindowClass::setURL(const QString& urlString) {
|
||||||
DependencyManager::get<OffscreenUi>()->executeOnUiThread([=] {
|
DependencyManager::get<OffscreenUi>()->executeOnUiThread([=] {
|
||||||
if (!_qmlWindow.isNull()) {
|
if (!_qmlWindow.isNull()) {
|
||||||
_qmlWindow->setProperty(URL_PROPERTY, fixupHifiUrl(urlString));
|
_qmlWindow->setProperty(URL_PROPERTY, urlString);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ class QmlWebWindowClass : public QmlWindowClass {
|
||||||
public:
|
public:
|
||||||
static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine);
|
static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
QString getURL() const;
|
QString getURL() const;
|
||||||
void setURL(const QString& url);
|
void setURL(const QString& url);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue