diff options
author | Allan Sandfeld Jensen <allan.jensen@theqtcompany.com> | 2016-01-07 16:13:16 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@theqtcompany.com> | 2016-01-15 10:44:48 +0000 |
commit | bc315ce05298cf500f45f3a897b0f7c0408fd611 (patch) | |
tree | 7560fbe0e18f63dcc62a5e9d0f328b3cb2b67bec | |
parent | 4bf31a52de2f9c8d049d2fd7410b9cfb88d41168 (diff) |
Add API to set WebChannel on isolated world
Make it possible to set a web-channel so that it can only be accessed
by private scripts.
Pulls in needed API extension in 3rdparty.
Task-number: QTBUG-50318
Change-Id: I61bcce5c318dffe0a406ee8cddf31f58a021c22c
Reviewed-by: Joerg Bornemann <joerg.bornemann@theqtcompany.com>
m--------- | src/3rdparty | 0 | ||||
-rw-r--r-- | src/core/common/qt_messages.h | 5 | ||||
-rw-r--r-- | src/core/renderer/web_channel_ipc_transport.cpp | 52 | ||||
-rw-r--r-- | src/core/renderer/web_channel_ipc_transport.h | 5 | ||||
-rw-r--r-- | src/core/web_channel_ipc_transport_host.cpp | 16 | ||||
-rw-r--r-- | src/core/web_channel_ipc_transport_host.h | 6 | ||||
-rw-r--r-- | src/core/web_contents_adapter.cpp | 17 | ||||
-rw-r--r-- | src/core/web_contents_adapter.h | 2 | ||||
-rw-r--r-- | src/core/web_contents_adapter_p.h | 1 | ||||
-rw-r--r-- | src/webengine/api/qquickwebengineview.cpp | 28 | ||||
-rw-r--r-- | src/webengine/api/qquickwebengineview_p.h | 4 | ||||
-rw-r--r-- | src/webengine/api/qquickwebengineview_p_p.h | 1 | ||||
-rw-r--r-- | src/webenginewidgets/api/qwebenginepage.cpp | 33 | ||||
-rw-r--r-- | src/webenginewidgets/api/qwebenginepage.h | 1 | ||||
-rw-r--r-- | tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp | 70 | ||||
-rw-r--r-- | tests/auto/widgets/resources/qwebchannel.js | 413 | ||||
-rw-r--r-- | tests/auto/widgets/resources/tests.qrc | 5 | ||||
-rw-r--r-- | tests/auto/widgets/tests.pri | 1 |
18 files changed, 626 insertions, 34 deletions
diff --git a/src/3rdparty b/src/3rdparty -Subproject e921076fcf1736db4aeb76877c6f608f42f4acc +Subproject 1334c7619425f44b0473c1c808ed1005fdb3e2a diff --git a/src/core/common/qt_messages.h b/src/core/common/qt_messages.h index 02f8716d6..386f8fc76 100644 --- a/src/core/common/qt_messages.h +++ b/src/core/common/qt_messages.h @@ -34,8 +34,9 @@ IPC_MESSAGE_ROUTED1(RenderViewObserverQt_FetchDocumentInnerText, IPC_MESSAGE_ROUTED1(RenderViewObserverQt_SetBackgroundColor, uint32 /* color */) -IPC_MESSAGE_ROUTED0(WebChannelIPCTransport_Install) -IPC_MESSAGE_ROUTED1(WebChannelIPCTransport_Message, std::vector<char> /*binaryJSON*/) +IPC_MESSAGE_ROUTED1(WebChannelIPCTransport_Install, uint /* worldId */) +IPC_MESSAGE_ROUTED1(WebChannelIPCTransport_Uninstall, uint /* worldId */) +IPC_MESSAGE_ROUTED2(WebChannelIPCTransport_Message, std::vector<char> /*binaryJSON*/, uint /* worldId */) // User scripts messages IPC_MESSAGE_ROUTED1(RenderViewObserverHelper_AddScript, diff --git a/src/core/renderer/web_channel_ipc_transport.cpp b/src/core/renderer/web_channel_ipc_transport.cpp index 12acd348e..43dc3cd81 100644 --- a/src/core/renderer/web_channel_ipc_transport.cpp +++ b/src/core/renderer/web_channel_ipc_transport.cpp @@ -57,7 +57,8 @@ namespace QtWebEngineCore { class WebChannelTransport : public gin::Wrappable<WebChannelTransport> { public: static gin::WrapperInfo kWrapperInfo; - static void Install(blink::WebFrame *frame); + static void Install(blink::WebFrame *frame, uint worldId); + static void Uninstall(blink::WebFrame *frame, uint worldId); private: content::RenderView *GetRenderView(v8::Isolate *isolate); WebChannelTransport() { } @@ -89,11 +90,15 @@ private: gin::WrapperInfo WebChannelTransport::kWrapperInfo = { gin::kEmbedderNativeGin }; -void WebChannelTransport::Install(blink::WebFrame *frame) +void WebChannelTransport::Install(blink::WebFrame *frame, uint worldId) { v8::Isolate *isolate = v8::Isolate::GetCurrent(); v8::HandleScope handleScope(isolate); - v8::Handle<v8::Context> context = frame->mainWorldScriptContext(); + v8::Handle<v8::Context> context; + if (worldId == 0) + context = frame->mainWorldScriptContext(); + else + context = frame->toWebLocalFrame()->isolatedWorldScriptContext(worldId, 0); v8::Context::Scope contextScope(context); gin::Handle<WebChannelTransport> transport = gin::CreateHandle(isolate, new WebChannelTransport); @@ -106,6 +111,24 @@ void WebChannelTransport::Install(blink::WebFrame *frame) qt->Set(gin::StringToV8(isolate, "webChannelTransport"), transport.ToV8()); } +void WebChannelTransport::Uninstall(blink::WebFrame *frame, uint worldId) +{ + v8::Isolate *isolate = v8::Isolate::GetCurrent(); + v8::HandleScope handleScope(isolate); + v8::Handle<v8::Context> context; + if (worldId == 0) + context = frame->mainWorldScriptContext(); + else + context = frame->toWebLocalFrame()->isolatedWorldScriptContext(worldId, 0); + v8::Context::Scope contextScope(context); + + v8::Handle<v8::Object> global(context->Global()); + v8::Handle<v8::Object> qt = global->Get(gin::StringToV8(isolate, "qt"))->ToObject(); + if (qt.IsEmpty()) + return; + qt->Delete(gin::StringToV8(isolate, "webChannelTransport")); +} + gin::ObjectTemplateBuilder WebChannelTransport::GetObjectTemplateBuilder(v8::Isolate *isolate) { return gin::Wrappable<WebChannelTransport>::GetObjectTemplateBuilder(isolate).SetMethod("send", &WebChannelTransport::NativeQtSendMessage); @@ -130,15 +153,23 @@ WebChannelIPCTransport::WebChannelIPCTransport(content::RenderView *renderView) { } -void WebChannelIPCTransport::installExtension() +void WebChannelIPCTransport::installWebChannel(uint worldId) +{ + blink::WebView *webView = render_view()->GetWebView(); + if (!webView) + return; + WebChannelTransport::Install(webView->mainFrame(), worldId); +} + +void WebChannelIPCTransport::uninstallWebChannel(uint worldId) { blink::WebView *webView = render_view()->GetWebView(); if (!webView) return; - WebChannelTransport::Install(webView->mainFrame()); + WebChannelTransport::Uninstall(webView->mainFrame(), worldId); } -void WebChannelIPCTransport::dispatchWebChannelMessage(const std::vector<char> &binaryJSON) +void WebChannelIPCTransport::dispatchWebChannelMessage(const std::vector<char> &binaryJSON, uint worldId) { blink::WebView *webView = render_view()->GetWebView(); if (!webView) @@ -151,7 +182,11 @@ void WebChannelIPCTransport::dispatchWebChannelMessage(const std::vector<char> & v8::Isolate *isolate = v8::Isolate::GetCurrent(); v8::HandleScope handleScope(isolate); blink::WebFrame *frame = webView->mainFrame(); - v8::Handle<v8::Context> context = frame->mainWorldScriptContext(); + v8::Handle<v8::Context> context; + if (worldId == 0) + context = frame->mainWorldScriptContext(); + else + context = frame->toWebLocalFrame()->isolatedWorldScriptContext(worldId, 0); v8::Context::Scope contextScope(context); v8::Handle<v8::Object> global(context->Global()); @@ -183,7 +218,8 @@ bool WebChannelIPCTransport::OnMessageReceived(const IPC::Message &message) { bool handled = true; IPC_BEGIN_MESSAGE_MAP(WebChannelIPCTransport, message) - IPC_MESSAGE_HANDLER(WebChannelIPCTransport_Install, installExtension) + IPC_MESSAGE_HANDLER(WebChannelIPCTransport_Install, installWebChannel) + IPC_MESSAGE_HANDLER(WebChannelIPCTransport_Uninstall, uninstallWebChannel) IPC_MESSAGE_HANDLER(WebChannelIPCTransport_Message, dispatchWebChannelMessage) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() diff --git a/src/core/renderer/web_channel_ipc_transport.h b/src/core/renderer/web_channel_ipc_transport.h index ba378f440..e5d65c358 100644 --- a/src/core/renderer/web_channel_ipc_transport.h +++ b/src/core/renderer/web_channel_ipc_transport.h @@ -52,8 +52,9 @@ public: WebChannelIPCTransport(content::RenderView *); private: - void dispatchWebChannelMessage(const std::vector<char> &binaryJSON); - void installExtension(); + void dispatchWebChannelMessage(const std::vector<char> &binaryJSON, uint worldId); + void installWebChannel(uint worldId); + void uninstallWebChannel(uint worldId); virtual bool OnMessageReceived(const IPC::Message &message) Q_DECL_OVERRIDE; }; diff --git a/src/core/web_channel_ipc_transport_host.cpp b/src/core/web_channel_ipc_transport_host.cpp index 800e78308..1e01c6e8e 100644 --- a/src/core/web_channel_ipc_transport_host.cpp +++ b/src/core/web_channel_ipc_transport_host.cpp @@ -46,23 +46,33 @@ namespace QtWebEngineCore { -WebChannelIPCTransportHost::WebChannelIPCTransportHost(content::WebContents *contents, QObject *parent) +WebChannelIPCTransportHost::WebChannelIPCTransportHost(content::WebContents *contents, uint worldId, QObject *parent) : QWebChannelAbstractTransport(parent) , content::WebContentsObserver(contents) + , m_worldId(worldId) { - Send(new WebChannelIPCTransport_Install(routing_id())); + Send(new WebChannelIPCTransport_Install(routing_id(), m_worldId)); } WebChannelIPCTransportHost::~WebChannelIPCTransportHost() { } +void WebChannelIPCTransportHost::setWorldId(uint worldId) +{ + if (worldId == m_worldId) + return; + Send(new WebChannelIPCTransport_Uninstall(routing_id(), m_worldId)); + m_worldId = worldId; + Send(new WebChannelIPCTransport_Install(routing_id(), m_worldId)); +} + void WebChannelIPCTransportHost::sendMessage(const QJsonObject &message) { QJsonDocument doc(message); int size = 0; const char *rawData = doc.rawData(&size); - Send(new WebChannelIPCTransport_Message(routing_id(), std::vector<char>(rawData, rawData + size))); + Send(new WebChannelIPCTransport_Message(routing_id(), std::vector<char>(rawData, rawData + size), m_worldId)); } void WebChannelIPCTransportHost::onWebChannelMessage(const std::vector<char> &message) diff --git a/src/core/web_channel_ipc_transport_host.h b/src/core/web_channel_ipc_transport_host.h index 9c21116f1..c84a0ee55 100644 --- a/src/core/web_channel_ipc_transport_host.h +++ b/src/core/web_channel_ipc_transport_host.h @@ -52,15 +52,19 @@ class WebChannelIPCTransportHost : public QWebChannelAbstractTransport , public content::WebContentsObserver { public: - WebChannelIPCTransportHost(content::WebContents *, QObject *parent = 0); + WebChannelIPCTransportHost(content::WebContents *, uint worldId = 0, QObject *parent = 0); virtual ~WebChannelIPCTransportHost(); // QWebChannelAbstractTransport virtual void sendMessage(const QJsonObject &message) Q_DECL_OVERRIDE; + void setWorldId(uint worldId); + uint worldId() const { return m_worldId; } + private: bool OnMessageReceived(const IPC::Message& message) Q_DECL_OVERRIDE; void onWebChannelMessage(const std::vector<char> &message); + uint m_worldId; }; } // namespace diff --git a/src/core/web_contents_adapter.cpp b/src/core/web_contents_adapter.cpp index fc77bdb9d..657a2eed3 100644 --- a/src/core/web_contents_adapter.cpp +++ b/src/core/web_contents_adapter.cpp @@ -315,6 +315,7 @@ WebContentsAdapterPrivate::WebContentsAdapterPrivate() // This has to be the first thing we create, and the last we destroy. : engineContext(WebEngineContext::current()) , webChannel(0) + , webChannelWorld(0) , adapterClient(0) , nextRequestId(CallbackDirectory::ReservedCallbackIdsEnd) , lastFindRequestId(0) @@ -961,17 +962,23 @@ QWebChannel *WebContentsAdapter::webChannel() const return d->webChannel; } -void WebContentsAdapter::setWebChannel(QWebChannel *channel) +void WebContentsAdapter::setWebChannel(QWebChannel *channel, uint worldId) { Q_D(WebContentsAdapter); - if (d->webChannel == channel) + if (d->webChannel == channel && d->webChannelWorld == worldId) return; + if (!d->webChannelTransport.get()) - d->webChannelTransport.reset(new WebChannelIPCTransportHost(d->webContents.get())); - else - d->webChannel->disconnectFrom(d->webChannelTransport.get()); + d->webChannelTransport.reset(new WebChannelIPCTransportHost(d->webContents.get(), worldId)); + else { + if (d->webChannel != channel) + d->webChannel->disconnectFrom(d->webChannelTransport.get()); + if (d->webChannelWorld != worldId) + d->webChannelTransport->setWorldId(worldId); + } d->webChannel = channel; + d->webChannelWorld = worldId; if (!channel) { d->webChannelTransport.reset(); return; diff --git a/src/core/web_contents_adapter.h b/src/core/web_contents_adapter.h index 90e035da1..ddb313c32 100644 --- a/src/core/web_contents_adapter.h +++ b/src/core/web_contents_adapter.h @@ -150,7 +150,7 @@ public: BrowserContextQt* browserContext(); BrowserContextAdapter* browserContextAdapter(); QWebChannel *webChannel() const; - void setWebChannel(QWebChannel *); + void setWebChannel(QWebChannel *, uint worldId); QPointF lastScrollOffset() const; QSizeF lastContentsSize() const; diff --git a/src/core/web_contents_adapter_p.h b/src/core/web_contents_adapter_p.h index 63f075bce..709cb8c2a 100644 --- a/src/core/web_contents_adapter_p.h +++ b/src/core/web_contents_adapter_p.h @@ -85,6 +85,7 @@ public: scoped_ptr<RenderViewObserverHostQt> renderViewObserverHost; scoped_ptr<WebChannelIPCTransportHost> webChannelTransport; QWebChannel *webChannel; + unsigned int webChannelWorld; WebContentsAdapterClient *adapterClient; quint64 nextRequestId; int lastFindRequestId; diff --git a/src/webengine/api/qquickwebengineview.cpp b/src/webengine/api/qquickwebengineview.cpp index 61a19faa5..2eef6a767 100644 --- a/src/webengine/api/qquickwebengineview.cpp +++ b/src/webengine/api/qquickwebengineview.cpp @@ -109,9 +109,10 @@ QQuickWebEngineViewPrivate::QQuickWebEngineViewPrivate() , isLoading(false) , m_activeFocusOnPress(true) , devicePixelRatio(QGuiApplication::primaryScreen()->devicePixelRatio()) + , m_webChannel(0) + , m_webChannelWorld(0) , m_dpiScale(1.0) , m_backgroundColor(Qt::white) - , m_webChannel(0) { // The gold standard for mobile web content is 160 dpi, and the devicePixelRatio expected // is the (possibly quantized) ratio of device dpi to 160 dpi. @@ -733,7 +734,7 @@ void QQuickWebEngineViewPrivate::adoptWebContents(WebContentsAdapter *webContent // associate the webChannel with the new adapter if (m_webChannel) - adapter->setWebChannel(m_webChannel); + adapter->setWebChannel(m_webChannel, m_webChannelWorld); // set initial background color if non-default if (m_backgroundColor != Qt::white) @@ -782,7 +783,7 @@ void QQuickWebEngineViewPrivate::ensureContentsAdapter() if (m_backgroundColor != Qt::white) adapter->backgroundColorChanged(); if (m_webChannel) - adapter->setWebChannel(m_webChannel); + adapter->setWebChannel(m_webChannel, m_webChannelWorld); if (explicitUrl.isValid()) adapter->load(explicitUrl); // push down the page's user scripts @@ -1209,7 +1210,7 @@ QQmlWebChannel *QQuickWebEngineView::webChannel() if (!d->m_webChannel) { d->m_webChannel = new QQmlWebChannel(this); if (d->adapter) - d->adapter->setWebChannel(d->m_webChannel); + d->adapter->setWebChannel(d->m_webChannel, d->m_webChannelWorld); } return d->m_webChannel; @@ -1222,10 +1223,27 @@ void QQuickWebEngineView::setWebChannel(QQmlWebChannel *webChannel) return; d->m_webChannel = webChannel; if (d->adapter) - d->adapter->setWebChannel(webChannel); + d->adapter->setWebChannel(webChannel, d->m_webChannelWorld); Q_EMIT webChannelChanged(); } +uint QQuickWebEngineView::webChannelWorld() const +{ + Q_D(const QQuickWebEngineView); + return d->m_webChannelWorld; +} + +void QQuickWebEngineView::setWebChannelWorld(uint webChannelWorld) +{ + Q_D(QQuickWebEngineView); + if (d->m_webChannelWorld == webChannelWorld) + return; + d->m_webChannelWorld = webChannelWorld; + if (d->adapter) + d->adapter->setWebChannel(d->m_webChannel, d->m_webChannelWorld); + Q_EMIT webChannelWorldChanged(webChannelWorld); +} + void QQuickWebEngineView::grantFeaturePermission(const QUrl &securityOrigin, QQuickWebEngineView::Feature feature, bool granted) { if (!d_ptr->adapter) diff --git a/src/webengine/api/qquickwebengineview_p.h b/src/webengine/api/qquickwebengineview_p.h index 43cdcb73e..7fdbafb77 100644 --- a/src/webengine/api/qquickwebengineview_p.h +++ b/src/webengine/api/qquickwebengineview_p.h @@ -111,6 +111,7 @@ class Q_WEBENGINE_PRIVATE_EXPORT QQuickWebEngineView : public QQuickItem { Q_PROPERTY(QSizeF contentsSize READ contentsSize NOTIFY contentsSizeChanged FINAL REVISION 3) Q_PROPERTY(QPointF scrollPosition READ scrollPosition NOTIFY scrollPositionChanged FINAL REVISION 3) Q_PROPERTY(bool audioMuted READ isAudioMuted WRITE setAudioMuted NOTIFY audioMutedChanged REVISION 3) + Q_PROPERTY(uint webChannelWorld READ webChannelWorld WRITE setWebChannelWorld NOTIFY webChannelWorldChanged REVISION 3) #ifdef ENABLE_QML_TESTSUPPORT_API Q_PROPERTY(QQuickWebEngineTestSupport *testSupport READ testSupport WRITE setTestSupport FINAL) @@ -274,6 +275,8 @@ public: QQmlWebChannel *webChannel(); void setWebChannel(QQmlWebChannel *); QQuickWebEngineHistory *navigationHistory() const; + uint webChannelWorld() const; + void setWebChannelWorld(uint); #ifdef ENABLE_QML_TESTSUPPORT_API QQuickWebEngineTestSupport *testSupport() const; @@ -329,6 +332,7 @@ Q_SIGNALS: Q_REVISION(3) void scrollPositionChanged(const QPointF& position); Q_REVISION(3) void audioMutedChanged(bool muted); Q_REVISION(3) void wasRecentlyAudibleChanged(bool wasRecentlyAudible); + Q_REVISION(3) void webChannelWorldChanged(uint); protected: void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry); diff --git a/src/webengine/api/qquickwebengineview_p_p.h b/src/webengine/api/qquickwebengineview_p_p.h index dd20c8972..6d72628a2 100644 --- a/src/webengine/api/qquickwebengineview_p_p.h +++ b/src/webengine/api/qquickwebengineview_p_p.h @@ -214,6 +214,7 @@ public: QMap<quint64, QJSValue> m_callbacks; QList<QSharedPointer<CertificateErrorController> > m_certificateErrorControllers; QQmlWebChannel *m_webChannel; + uint m_webChannelWorld; private: QScopedPointer<QtWebEngineCore::UIDelegatesManager> m_uIDelegatesManager; diff --git a/src/webenginewidgets/api/qwebenginepage.cpp b/src/webenginewidgets/api/qwebenginepage.cpp index d97cb5a09..23ab9a244 100644 --- a/src/webenginewidgets/api/qwebenginepage.cpp +++ b/src/webenginewidgets/api/qwebenginepage.cpp @@ -551,7 +551,7 @@ QWebEngineSettings *QWebEnginePage::settings() const * that is exposed in the JavaScript context of this page as \c qt.webChannelTransport. * * \since 5.5 - * \sa QWebChannel + * \sa setWebChannel */ QWebChannel *QWebEnginePage::webChannel() const { @@ -560,20 +560,41 @@ QWebChannel *QWebEnginePage::webChannel() const } /*! + * \overload + * + * Sets the web channel instance to be used by this page to \a channel and installs + * it in the main JavaScript world. + * + * With this method the web channel can be accessed by web page content. If the content + * is not under your control and might be hostile, this could be a security issue and + * you should consider installing it in a private JavaScript world. + * + * \since 5.5 + * \sa QWebEngineScript::MainWorld + */ + +void QWebEnginePage::setWebChannel(QWebChannel *channel) +{ + setWebChannel(channel, QWebEngineScript::MainWorld); +} + +/*! * Sets the web channel instance to be used by this page to \a channel and connects it to * web engine's transport using Chromium IPC messages. The transport is exposed in the JavaScript - * context of this page as + * world \a worldId as * \c qt.webChannelTransport, which should be used when using the \l{Qt WebChannel JavaScript API}. * * \note The page does not take ownership of the channel object. + * \note Only one web channel can be installed per page, setting one even in another JavaScript + * world uninstalls any already installed web channel. * - * \since 5.5 + * \since 5.7 + * \sa QWebEngineScript::ScriptWorldId */ - -void QWebEnginePage::setWebChannel(QWebChannel *channel) +void QWebEnginePage::setWebChannel(QWebChannel *channel, uint worldId) { Q_D(QWebEnginePage); - d->adapter->setWebChannel(channel); + d->adapter->setWebChannel(channel, worldId); } /*! diff --git a/src/webenginewidgets/api/qwebenginepage.h b/src/webenginewidgets/api/qwebenginepage.h index 950ae374a..c25d3d452 100644 --- a/src/webenginewidgets/api/qwebenginepage.h +++ b/src/webenginewidgets/api/qwebenginepage.h @@ -248,6 +248,7 @@ public: QWebChannel *webChannel() const; void setWebChannel(QWebChannel *); + void setWebChannel(QWebChannel *, uint worldId); QColor backgroundColor() const; void setBackgroundColor(const QColor &color); diff --git a/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp b/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp index 3231293bf..ad10234f4 100644 --- a/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp +++ b/tests/auto/widgets/qwebenginescript/tst_qwebenginescript.cpp @@ -24,6 +24,7 @@ #include <qwebenginescriptcollection.h> #include <qwebengineview.h> #include "../util.h" +#include <QWebChannel> class tst_QWebEngineScript: public QObject { Q_OBJECT @@ -34,7 +35,8 @@ private Q_SLOTS: void injectionPoint_data(); void scriptWorld(); void scriptModifications(); - + void webChannel_data(); + void webChannel(); }; void tst_QWebEngineScript::domEditing() @@ -150,6 +152,72 @@ void tst_QWebEngineScript::scriptModifications() QVERIFY(page.scripts().count() == 0); } +class TestObject : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) +public: + TestObject(QObject *parent = 0) : QObject(parent) { } + + void setText(const QString &text) + { + if (text == m_text) + return; + m_text = text; + emit textChanged(text); + } + + QString text() const { return m_text; } + +signals: + void textChanged(const QString &text); + +private: + QString m_text; +}; + + +void tst_QWebEngineScript::webChannel_data() +{ + QTest::addColumn<int>("worldId"); + QTest::newRow("MainWorld") << static_cast<int>(QWebEngineScript::MainWorld); + QTest::newRow("ApplicationWorld") << static_cast<int>(QWebEngineScript::ApplicationWorld); +} + +void tst_QWebEngineScript::webChannel() +{ + QFETCH(int, worldId); + QWebEnginePage page; + TestObject testObject; + QScopedPointer<QWebChannel> channel(new QWebChannel(this)); + channel->registerObject(QStringLiteral("object"), &testObject); + page.setWebChannel(channel.data(), worldId); + + QFile qwebchanneljs(":/qwebchannel.js"); + QVERIFY(qwebchanneljs.exists()); + qwebchanneljs.open(QFile::ReadOnly); + QByteArray scriptSrc = qwebchanneljs.readAll(); + qwebchanneljs.close(); + QWebEngineScript script; + script.setInjectionPoint(QWebEngineScript::DocumentCreation); + script.setWorldId(worldId); + script.setSourceCode(QString::fromLatin1(scriptSrc)); + page.scripts().insert(script); + page.setHtml(QStringLiteral("<html><body></body></html>")); + waitForSignal(&page, SIGNAL(loadFinished(bool))); + page.runJavaScript(QLatin1String( + "new QWebChannel(qt.webChannelTransport," + " function(channel) {" + " channel.objects.object.text = 'test';" + " }" + ");"), worldId); + waitForSignal(&testObject, SIGNAL(textChanged(QString))); + QCOMPARE(testObject.text(), QStringLiteral("test")); + + if (worldId != QWebEngineScript::MainWorld) + QCOMPARE(evaluateJavaScriptSync(&page, "qt.webChannelTransport"), QVariant(QVariant::Invalid)); +} + QTEST_MAIN(tst_QWebEngineScript) #include "tst_qwebenginescript.moc" diff --git a/tests/auto/widgets/resources/qwebchannel.js b/tests/auto/widgets/resources/qwebchannel.js new file mode 100644 index 000000000..d8c28bc66 --- /dev/null +++ b/tests/auto/widgets/resources/qwebchannel.js @@ -0,0 +1,413 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +"use strict"; + +var QWebChannelMessageTypes = { + signal: 1, + propertyUpdate: 2, + init: 3, + idle: 4, + debug: 5, + invokeMethod: 6, + connectToSignal: 7, + disconnectFromSignal: 8, + setProperty: 9, + response: 10, +}; + +var QWebChannel = function(transport, initCallback) +{ + if (typeof transport !== "object" || typeof transport.send !== "function") { + console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + + " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); + return; + } + + var channel = this; + this.transport = transport; + + this.send = function(data) + { + if (typeof(data) !== "string") { + data = JSON.stringify(data); + } + channel.transport.send(data); + } + + this.transport.onmessage = function(message) + { + var data = message.data; + if (typeof data === "string") { + data = JSON.parse(data); + } + switch (data.type) { + case QWebChannelMessageTypes.signal: + channel.handleSignal(data); + break; + case QWebChannelMessageTypes.response: + channel.handleResponse(data); + break; + case QWebChannelMessageTypes.propertyUpdate: + channel.handlePropertyUpdate(data); + break; + default: + console.error("invalid message received:", message.data); + break; + } + } + + this.execCallbacks = {}; + this.execId = 0; + this.exec = function(data, callback) + { + if (!callback) { + // if no callback is given, send directly + channel.send(data); + return; + } + if (channel.execId === Number.MAX_VALUE) { + // wrap + channel.execId = Number.MIN_VALUE; + } + if (data.hasOwnProperty("id")) { + console.error("Cannot exec message with property id: " + JSON.stringify(data)); + return; + } + data.id = channel.execId++; + channel.execCallbacks[data.id] = callback; + channel.send(data); + }; + + this.objects = {}; + + this.handleSignal = function(message) + { + var object = channel.objects[message.object]; + if (object) { + object.signalEmitted(message.signal, message.args); + } else { + console.warn("Unhandled signal: " + message.object + "::" + message.signal); + } + } + + this.handleResponse = function(message) + { + if (!message.hasOwnProperty("id")) { + console.error("Invalid response message received: ", JSON.stringify(message)); + return; + } + channel.execCallbacks[message.id](message.data); + delete channel.execCallbacks[message.id]; + } + + this.handlePropertyUpdate = function(message) + { + for (var i in message.data) { + var data = message.data[i]; + var object = channel.objects[data.object]; + if (object) { + object.propertyUpdate(data.signals, data.properties); + } else { + console.warn("Unhandled property update: " + data.object + "::" + data.signal); + } + } + channel.exec({type: QWebChannelMessageTypes.idle}); + } + + this.debug = function(message) + { + channel.send({type: QWebChannelMessageTypes.debug, data: message}); + }; + + channel.exec({type: QWebChannelMessageTypes.init}, function(data) { + for (var objectName in data) { + var object = new QObject(objectName, data[objectName], channel); + } + // now unwrap properties, which might reference other registered objects + for (var objectName in channel.objects) { + channel.objects[objectName].unwrapProperties(); + } + if (initCallback) { + initCallback(channel); + } + channel.exec({type: QWebChannelMessageTypes.idle}); + }); +}; + +function QObject(name, data, webChannel) +{ + this.__id__ = name; + webChannel.objects[name] = this; + + // List of callbacks that get invoked upon signal emission + this.__objectSignals__ = {}; + + // Cache of all properties, updated when a notify signal is emitted + this.__propertyCache__ = {}; + + var object = this; + + // ---------------------------------------------------------------------- + + this.unwrapQObject = function(response) + { + if (response instanceof Array) { + // support list of objects + var ret = new Array(response.length); + for (var i = 0; i < response.length; ++i) { + ret[i] = object.unwrapQObject(response[i]); + } + return ret; + } + if (!response + || !response["__QObject*__"] + || response.id === undefined) { + return response; + } + + var objectId = response.id; + if (webChannel.objects[objectId]) + return webChannel.objects[objectId]; + + if (!response.data) { + console.error("Cannot unwrap unknown QObject " + objectId + " without data."); + return; + } + + var qObject = new QObject( objectId, response.data, webChannel ); + qObject.destroyed.connect(function() { + if (webChannel.objects[objectId] === qObject) { + delete webChannel.objects[objectId]; + // reset the now deleted QObject to an empty {} object + // just assigning {} though would not have the desired effect, but the + // below also ensures all external references will see the empty map + // NOTE: this detour is necessary to workaround QTBUG-40021 + var propertyNames = []; + for (var propertyName in qObject) { + propertyNames.push(propertyName); + } + for (var idx in propertyNames) { + delete qObject[propertyNames[idx]]; + } + } + }); + // here we are already initialized, and thus must directly unwrap the properties + qObject.unwrapProperties(); + return qObject; + } + + this.unwrapProperties = function() + { + for (var propertyIdx in object.__propertyCache__) { + object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); + } + } + + function addSignal(signalData, isPropertyNotifySignal) + { + var signalName = signalData[0]; + var signalIndex = signalData[1]; + object[signalName] = { + connect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to connect to signal " + signalName); + return; + } + + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + object.__objectSignals__[signalIndex].push(callback); + + if (!isPropertyNotifySignal && signalName !== "destroyed") { + // only required for "pure" signals, handled separately for properties in propertyUpdate + // also note that we always get notified about the destroyed signal + webChannel.exec({ + type: QWebChannelMessageTypes.connectToSignal, + object: object.__id__, + signal: signalIndex + }); + } + }, + disconnect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to disconnect from signal " + signalName); + return; + } + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + var idx = object.__objectSignals__[signalIndex].indexOf(callback); + if (idx === -1) { + console.error("Cannot find connection of signal " + signalName + " to " + callback.name); + return; + } + object.__objectSignals__[signalIndex].splice(idx, 1); + if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { + // only required for "pure" signals, handled separately for properties in propertyUpdate + webChannel.exec({ + type: QWebChannelMessageTypes.disconnectFromSignal, + object: object.__id__, + signal: signalIndex + }); + } + } + }; + } + + /** + * Invokes all callbacks for the given signalname. Also works for property notify callbacks. + */ + function invokeSignalCallbacks(signalName, signalArgs) + { + var connections = object.__objectSignals__[signalName]; + if (connections) { + connections.forEach(function(callback) { + callback.apply(callback, signalArgs); + }); + } + } + + this.propertyUpdate = function(signals, propertyMap) + { + // update property cache + for (var propertyIndex in propertyMap) { + var propertyValue = propertyMap[propertyIndex]; + object.__propertyCache__[propertyIndex] = propertyValue; + } + + for (var signalName in signals) { + // Invoke all callbacks, as signalEmitted() does not. This ensures the + // property cache is updated before the callbacks are invoked. + invokeSignalCallbacks(signalName, signals[signalName]); + } + } + + this.signalEmitted = function(signalName, signalArgs) + { + invokeSignalCallbacks(signalName, signalArgs); + } + + function addMethod(methodData) + { + var methodName = methodData[0]; + var methodIdx = methodData[1]; + object[methodName] = function() { + var args = []; + var callback; + for (var i = 0; i < arguments.length; ++i) { + if (typeof arguments[i] === "function") + callback = arguments[i]; + else + args.push(arguments[i]); + } + + webChannel.exec({ + "type": QWebChannelMessageTypes.invokeMethod, + "object": object.__id__, + "method": methodIdx, + "args": args + }, function(response) { + if (response !== undefined) { + var result = object.unwrapQObject(response); + if (callback) { + (callback)(result); + } + } + }); + }; + } + + function bindGetterSetter(propertyInfo) + { + var propertyIndex = propertyInfo[0]; + var propertyName = propertyInfo[1]; + var notifySignalData = propertyInfo[2]; + // initialize property cache with current value + // NOTE: if this is an object, it is not directly unwrapped as it might + // reference other QObject that we do not know yet + object.__propertyCache__[propertyIndex] = propertyInfo[3]; + + if (notifySignalData) { + if (notifySignalData[0] === 1) { + // signal name is optimized away, reconstruct the actual name + notifySignalData[0] = propertyName + "Changed"; + } + addSignal(notifySignalData, true); + } + + Object.defineProperty(object, propertyName, { + configurable: true, + get: function () { + var propertyValue = object.__propertyCache__[propertyIndex]; + if (propertyValue === undefined) { + // This shouldn't happen + console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); + } + + return propertyValue; + }, + set: function(value) { + if (value === undefined) { + console.warn("Property setter for " + propertyName + " called with undefined value!"); + return; + } + object.__propertyCache__[propertyIndex] = value; + webChannel.exec({ + "type": QWebChannelMessageTypes.setProperty, + "object": object.__id__, + "property": propertyIndex, + "value": value + }); + } + }); + + } + + // ---------------------------------------------------------------------- + + data.methods.forEach(addMethod); + + data.properties.forEach(bindGetterSetter); + + data.signals.forEach(function(signal) { addSignal(signal, false); }); + + for (var name in data.enums) { + object[name] = data.enums[name]; + } +} + +//required for use with nodejs +if (typeof module === 'object') { + module.exports = { + QWebChannel: QWebChannel + }; +} diff --git a/tests/auto/widgets/resources/tests.qrc b/tests/auto/widgets/resources/tests.qrc new file mode 100644 index 000000000..5e9df2873 --- /dev/null +++ b/tests/auto/widgets/resources/tests.qrc @@ -0,0 +1,5 @@ +<RCC> + <qresource prefix="/"> + <file>qwebchannel.js</file> + </qresource> +</RCC> diff --git a/tests/auto/widgets/tests.pri b/tests/auto/widgets/tests.pri index 8a62ce2a6..ca19a9496 100644 --- a/tests/auto/widgets/tests.pri +++ b/tests/auto/widgets/tests.pri @@ -9,6 +9,7 @@ TARGET = tst_$$TARGET SOURCES += $${TARGET}.cpp INCLUDEPATH += $$PWD +RESOURCES += ../resources/tests.qrc exists($$_PRO_FILE_PWD_/$${TARGET}.qrc): RESOURCES += $${TARGET}.qrc QT += testlib network webenginewidgets widgets |