Commit 13184a2d authored by Zain Afzal's avatar Zain Afzal Committed by Commit Bot

Added message interface between chrome://media-app and chrome://media-app-guest

Todate, we've only passed a single, simple message to open a file from media-app
to media-app-guest. To better support our use-cases we need a bidirectional pipe,
and some abstractions around the dispatching of messages.

This CL introduces a MessagePipe class to encapsulate the communication channel
and manage dispatch of requests and responses between the privileged and
unprivileged JS contexts in chrome://media-app. It still requires explicit
definitions of what messages either side can send and receive but allows for
these messages to be defined and used with less friction.

Bug: 996088, 1040328
Change-Id: I6179a5d176d74f0f37fa1809363e87a0ed2976e5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2049404
Commit-Queue: Zain Afzal <zafzal@google.com>
Reviewed-by: default avatarTrent Apted <tapted@chromium.org>
Cr-Commit-Position: refs/heads/master@{#746214}
parent 208ec7b2
...@@ -69,6 +69,7 @@ source_set("browser_test_support") { ...@@ -69,6 +69,7 @@ source_set("browser_test_support") {
"test/driver.js", "test/driver.js",
"test/driver_api.js", "test/driver_api.js",
"test/guest_query_receiver.js", "test/guest_query_receiver.js",
"test/receiver_api.js",
] ]
} }
...@@ -98,6 +99,7 @@ js_type_check("closure_compile_tests") { ...@@ -98,6 +99,7 @@ js_type_check("closure_compile_tests") {
":test_driver_api_js", ":test_driver_api_js",
":test_driver_js", ":test_driver_js",
":test_guest_query_receiver_js", ":test_guest_query_receiver_js",
":test_receiver_api_js",
] ]
} }
...@@ -106,10 +108,19 @@ js_library("test_driver_api_js") { ...@@ -106,10 +108,19 @@ js_library("test_driver_api_js") {
sources = [ "test/driver_api.js" ] sources = [ "test/driver_api.js" ]
} }
js_library("test_receiver_api_js") {
testonly = true
sources = [ "test/receiver_api.js" ]
}
js_library("test_guest_query_receiver_js") { js_library("test_guest_query_receiver_js") {
testonly = true testonly = true
sources = [ "test/guest_query_receiver.js" ] sources = [ "test/guest_query_receiver.js" ]
deps = [ ":test_driver_api_js" ] deps = [
":test_driver_api_js",
":test_receiver_api_js",
"//chromeos/components/media_app_ui/resources/js:message_pipe",
]
} }
js_library("test_driver_js") { js_library("test_driver_js") {
...@@ -117,6 +128,7 @@ js_library("test_driver_js") { ...@@ -117,6 +128,7 @@ js_library("test_driver_js") {
sources = [ "test/driver.js" ] sources = [ "test/driver.js" ]
deps = [ deps = [
":test_driver_api_js", ":test_driver_api_js",
"//chromeos/components/media_app_ui/resources/js:message_pipe",
"//ui/webui/resources/js:assert", "//ui/webui/resources/js:assert",
] ]
} }
...@@ -21,6 +21,7 @@ content::WebUIDataSource* MediaAppGuestUI::CreateDataSource() { ...@@ -21,6 +21,7 @@ content::WebUIDataSource* MediaAppGuestUI::CreateDataSource() {
// Add resources from chromeos_media_app_resources.pak. // Add resources from chromeos_media_app_resources.pak.
source->AddResourcePath("app.html", IDR_MEDIA_APP_APP_HTML); source->AddResourcePath("app.html", IDR_MEDIA_APP_APP_HTML);
source->AddResourcePath("receiver.js", IDR_MEDIA_APP_RECEIVER_JS); source->AddResourcePath("receiver.js", IDR_MEDIA_APP_RECEIVER_JS);
source->AddResourcePath("message_pipe.js", IDR_MEDIA_APP_MESSAGE_PIPE_JS);
// Add resources from chromeos_media_app_bundle_resources.pak that are also // Add resources from chromeos_media_app_bundle_resources.pak that are also
// needed for mocks. If enable_cros_media_app = true, then these calls will // needed for mocks. If enable_cros_media_app = true, then these calls will
......
...@@ -31,6 +31,7 @@ content::WebUIDataSource* CreateHostDataSource() { ...@@ -31,6 +31,7 @@ content::WebUIDataSource* CreateHostDataSource() {
IDR_MEDIA_APP_MOJO_API_BOOTSTRAP_JS); IDR_MEDIA_APP_MOJO_API_BOOTSTRAP_JS);
source->AddResourcePath("media_app.mojom-lite.js", source->AddResourcePath("media_app.mojom-lite.js",
IDR_MEDIA_APP_MEDIA_APP_MOJOM_JS); IDR_MEDIA_APP_MEDIA_APP_MOJOM_JS);
source->AddResourcePath("message_pipe.js", IDR_MEDIA_APP_MESSAGE_PIPE_JS);
// Add resources from chromeos_media_app_bundle_resources.pak. // Add resources from chromeos_media_app_bundle_resources.pak.
source->AddResourcePath("system_assets/app_icon_256.png", source->AddResourcePath("system_assets/app_icon_256.png",
......
...@@ -10,4 +10,5 @@ ...@@ -10,4 +10,5 @@
} }
</style> </style>
<script src="/js/app_main.js"></script> <script src="/js/app_main.js"></script>
<script src="/message_pipe.js"></script>
<script src="/receiver.js"></script> <script src="/receiver.js"></script>
...@@ -61,4 +61,5 @@ ...@@ -61,4 +61,5 @@
<div id="prev-container" class="navigation-button"></div> <div id="prev-container" class="navigation-button"></div>
<div id="next-container" class="navigation-button"></div> <div id="next-container" class="navigation-button"></div>
</div> </div>
<script src="/message_pipe.js"></script>
<script src="/launch.js"></script> <script src="/launch.js"></script>
...@@ -19,6 +19,7 @@ js_type_check("closure_compile") { ...@@ -19,6 +19,7 @@ js_type_check("closure_compile") {
] ]
deps = [ deps = [
":launch", ":launch",
":message_pipe",
":mojo_api_bootstrap", ":mojo_api_bootstrap",
":receiver", ":receiver",
] ]
...@@ -29,10 +30,15 @@ js_library("launch") { ...@@ -29,10 +30,15 @@ js_library("launch") {
"web_app_file_handling.externs.js", "web_app_file_handling.externs.js",
"dom_draft.externs.js", "dom_draft.externs.js",
] ]
deps = [ ":message_pipe" ]
}
js_library("message_pipe") {
} }
js_library("receiver") { js_library("receiver") {
externs_list = [ "media_app.externs.js" ] externs_list = [ "media_app.externs.js" ]
deps = [ ":message_pipe" ]
} }
js_library("mojo_api_bootstrap") { js_library("mojo_api_bootstrap") {
......
...@@ -16,27 +16,16 @@ const currentFiles = []; ...@@ -16,27 +16,16 @@ const currentFiles = [];
*/ */
let entryIndex = -1; let entryIndex = -1;
/** /** A pipe through which we can send messages to the guest frame. */
* Helper to call postMessage on the guest frame. const guestMessagePipe = new MessagePipe('chrome://media-app-guest');
*
* @param {Object} message
*/
function postToGuestWindow(message) {
const guest =
/** @type{HTMLIFrameElement} */ (document.querySelector('iframe'));
// The next line should probably be passing a transfer argument, but that
// causes Chrome to send a "null" message. The transfer seems to work without
// the third argument (but inefficiently, perhaps).
guest.contentWindow.postMessage(message, 'chrome://media-app-guest');
}
/** /**
* Loads a file in the guest. * Loads a file in the guest.
* *
* @param {File} file * @param {File} file
*/ */
async function loadFile(file) { function loadFile(file) {
postToGuestWindow({'file': file}); guestMessagePipe.sendMessage('file', {'file': file});
} }
/** /**
......
This diff is collapsed.
...@@ -15,6 +15,16 @@ class SingleArrayBufferFileList { ...@@ -15,6 +15,16 @@ class SingleArrayBufferFileList {
} }
} }
/** A pipe through which we can send messages to the parent frame. */
const parentMessagePipe = new MessagePipe('chrome://media-app', window.parent);
parentMessagePipe.registerHandler('file', (message) => {
const fileMessage = /** @type mediaApp.MessageEventData */ (message);
if (fileMessage.file) {
loadFile(fileMessage.file);
}
})
/** /**
* Loads files associated with a message received from the host. * Loads files associated with a message received from the host.
* @param {!File} file * @param {!File} file
...@@ -30,32 +40,12 @@ async function loadFile(file) { ...@@ -30,32 +40,12 @@ async function loadFile(file) {
const app = /** @type {?mediaApp.ClientApi} */ ( const app = /** @type {?mediaApp.ClientApi} */ (
document.querySelector('backlight-app')); document.querySelector('backlight-app'));
if (app) { if (app) {
app.loadFiles(fileList); await app.loadFiles(fileList);
} else { } else {
window.customLaunchData = {files: fileList}; window.customLaunchData = {files: fileList};
} }
} }
function receiveMessage(/** Event */ e) {
const event = /** @type{MessageEvent<Object>} */ (e);
if (event.origin !== 'chrome://media-app') {
return;
}
// First ensure the message is our MessageEventData type, then act on it
// appropriately. Note test messages won't have a file (and are not handled by
// this listener), so it's currently sufficient to just check for `file`.
if ('file' in event.data) {
const message =
/** @type{MessageEvent<mediaApp.MessageEventData>}*/ (event);
if (message.data.file) {
loadFile(message.data.file);
} else {
console.error('Unknown message:', message);
}
}
}
// Attempting to execute chooseFileSystemEntries is guaranteed to result in a // Attempting to execute chooseFileSystemEntries is guaranteed to result in a
// SecurityError due to the fact that we are running in a unprivileged iframe. // SecurityError due to the fact that we are running in a unprivileged iframe.
// Note, we can not do window.chooseFileSystemEntries due to the fact that // Note, we can not do window.chooseFileSystemEntries due to the fact that
...@@ -63,5 +53,3 @@ function receiveMessage(/** Event */ e) { ...@@ -63,5 +53,3 @@ function receiveMessage(/** Event */ e) {
// TODO(crbug/1040328): Remove this when we have a polyfill that allows us to // TODO(crbug/1040328): Remove this when we have a polyfill that allows us to
// talk to the privileged frame. // talk to the privileged frame.
window['chooseFileSystemEntries'] = null; window['chooseFileSystemEntries'] = null;
window.addEventListener('message', receiveMessage, false);
...@@ -19,6 +19,9 @@ ...@@ -19,6 +19,9 @@
<!-- Unprivileged guest contents. --> <!-- Unprivileged guest contents. -->
<include name="IDR_MEDIA_APP_APP_HTML" file="app.html" type="BINDATA" /> <include name="IDR_MEDIA_APP_APP_HTML" file="app.html" type="BINDATA" />
<include name="IDR_MEDIA_APP_RECEIVER_JS" file="js/receiver.js" type="BINDATA" /> <include name="IDR_MEDIA_APP_RECEIVER_JS" file="js/receiver.js" type="BINDATA" />
<!-- Common contents. -->
<include name="IDR_MEDIA_APP_MESSAGE_PIPE_JS" file="js/message_pipe.js" type="BINDATA" />
</includes> </includes>
</release> </release>
</grit> </grit>
...@@ -4,51 +4,6 @@ ...@@ -4,51 +4,6 @@
/** Host-side of web-driver like controller for sandboxed guest frames. */ /** Host-side of web-driver like controller for sandboxed guest frames. */
class GuestDriver { class GuestDriver {
/** @param {string} origin */
constructor(origin) {
this.origin = origin;
/** @type {Array<MessageEvent<TestMessageResponseData>>} */
this.testMessageQueue = [];
/** @type {?function(MessageEvent<TestMessageResponseData>)} */
this.testMessageWaiter = null;
this.messageListener = (/** Event */ event) => {
const testEvent =
/** @type{MessageEvent<TestMessageResponseData>} */ (event);
console.log('Event from guest: ' + JSON.stringify(testEvent.data));
if (this.testMessageWaiter) {
this.testMessageWaiter(testEvent);
this.testMessageWaiter = null;
} else {
this.testMessageQueue.push(testEvent);
}
};
window.addEventListener('message', this.messageListener);
}
tearDown() {
window.removeEventListener('message', this.messageListener);
}
/**
* Returns the next message from the guest.
* @return {Promise<MessageEvent<TestMessageResponseData>>}
*/
popTestMessage() {
if (this.testMessageQueue.length > 0) {
const front = this.testMessageQueue[0];
this.testMessageQueue.shift();
return Promise.resolve(front);
}
return new Promise(resolve => {
this.testMessageWaiter = resolve;
});
}
/** /**
* Sends a query to the guest that repeatedly runs a query selector until * Sends a query to the guest that repeatedly runs a query selector until
* it returns an element. * it returns an element.
...@@ -60,13 +15,12 @@ class GuestDriver { ...@@ -60,13 +15,12 @@ class GuestDriver {
* tagName if unspecified. * tagName if unspecified.
*/ */
async waitForElementInGuest(query, opt_property, opt_commands = {}) { async waitForElementInGuest(query, opt_property, opt_commands = {}) {
const frame = assertInstanceof(
document.querySelector(`iframe[src^="${this.origin}"]`),
HTMLIFrameElement);
/** @type{TestMessageQueryData} */ /** @type{TestMessageQueryData} */
const message = {testQuery: query, property: opt_property}; const message = {testQuery: query, property: opt_property};
frame.contentWindow.postMessage({...message, ...opt_commands}, this.origin);
const event = await this.popTestMessage(); const result = /** @type{TestMessageResponseData} */ (
return event.data.testQueryResult; await guestMessagePipe.sendMessage(
'test', {...message, ...opt_commands}));
return result.testQueryResult;
} }
} }
...@@ -13,3 +13,6 @@ var TestMessageResponseData; ...@@ -13,3 +13,6 @@ var TestMessageResponseData;
* requestFullscreen: (boolean|undefined)}} * requestFullscreen: (boolean|undefined)}}
*/ */
var TestMessageQueryData; var TestMessageQueryData;
/** @type {MessagePipe} */
var guestMessagePipe;
\ No newline at end of file
...@@ -40,12 +40,13 @@ async function waitForNode(query, opt_path) { ...@@ -40,12 +40,13 @@ async function waitForNode(query, opt_path) {
/** /**
* Acts on received TestMessageQueryData. * Acts on received TestMessageQueryData.
* *
* @param {MessageEvent<TestMessageQueryData>} event * @param {TestMessageQueryData} data
* @return {!Promise<TestMessageResponseData>}
*/ */
async function runTestQuery(event) { async function runTestQuery(data) {
const data = event.data;
const element = await waitForNode(data.testQuery, data.pathToRoot || []); const element = await waitForNode(data.testQuery, data.pathToRoot || []);
let result = element.tagName; let result = element.tagName;
if (data.property) { if (data.property) {
result = JSON.stringify(element[data.property]); result = JSON.stringify(element[data.property]);
} else if (data.requestFullscreen) { } else if (data.requestFullscreen) {
...@@ -57,17 +58,23 @@ async function runTestQuery(event) { ...@@ -57,17 +58,23 @@ async function runTestQuery(event) {
} }
} }
const response = {testQueryResult: result}; return {testQueryResult: result};
event.source.postMessage(response, event.origin);
}
function receiveTestMessage(/** Event */ e) {
const event = /** @type{MessageEvent<TestMessageQueryData>} */ (e);
if (event.data.testQuery) {
runTestQuery(event);
}
} }
window.addEventListener('message', receiveTestMessage, false); // Wait until dom content has loaded to make sure receiver.js has been
// parsed and executed.
window.addEventListener('DOMContentLoaded', () => {
parentMessagePipe.registerHandler('test', (data) => {
return runTestQuery(/** @type{TestMessageQueryData} */ (data));
});
// Turn off error rethrowing for tests so the test runner doesn't mark
// our error handling tests as failed.
parentMessagePipe.rethrowErrors = false;
// Handler that will always error for helping to test the message pipe
// itself.
parentMessagePipe.registerHandler('bad-handler', () => {
throw Error('This is an error');
});
});
//# sourceURL=guest_query_reciever.js //# sourceURL=guest_query_receiver.js
...@@ -28,9 +28,14 @@ var MediaAppGuestUIBrowserTest = class extends testing.Test { ...@@ -28,9 +28,14 @@ var MediaAppGuestUIBrowserTest = class extends testing.Test {
preLoad() { preLoad() {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const mojoBindingsLite = document.createElement('script'); const mojoBindingsLite = document.createElement('script');
mojoBindingsLite.src = 'chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js'; mojoBindingsLite.src =
'chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js';
document.head.appendChild(mojoBindingsLite); document.head.appendChild(mojoBindingsLite);
}); });
// The guest will try and create a message pipe to its parent, since there
// is no containing frame here, window.parent === parent. This line mocks
// a wrapping iframe so the message pipe can still instantiate correctly.
window.parent = document.createElement('iframe');
} }
}; };
...@@ -46,4 +51,4 @@ TEST_F('MediaAppGuestUIBrowserTest', 'GuestCanSpawnWorkers', () => { ...@@ -46,4 +51,4 @@ TEST_F('MediaAppGuestUIBrowserTest', 'GuestCanSpawnWorkers', () => {
} }
assertEquals(error, null); assertEquals(error, null);
}); });
\ No newline at end of file
...@@ -61,12 +61,7 @@ var MediaAppUIBrowserTest = class extends testing.Test { ...@@ -61,12 +61,7 @@ var MediaAppUIBrowserTest = class extends testing.Test {
/** @override */ /** @override */
setUp() { setUp() {
super.setUp(); super.setUp();
driver = new GuestDriver(GUEST_ORIGIN); driver = new GuestDriver();
}
/** @override */
tearDown() {
driver.tearDown();
} }
}; };
...@@ -144,3 +139,31 @@ TEST_F('MediaAppUIBrowserTest', 'CanFullscreenVideo', async () => { ...@@ -144,3 +139,31 @@ TEST_F('MediaAppUIBrowserTest', 'CanFullscreenVideo', async () => {
testDone(); testDone();
}); });
// Tests that we receive an error if our message is unhandled.
TEST_F('MediaAppUIBrowserTest', 'ReceivesNoHandlerError', async () => {
let errorMessage = '';
try {
await guestMessagePipe.sendMessage('unknown-message', null);
} catch (error) {
errorMessage = error.message;
}
assertEquals(
errorMessage,
'No handler registered for message type \'unknown-message\'');
testDone();
});
// Tests that we receive an error if the handler fails.
TEST_F('MediaAppUIBrowserTest', 'ReceivesProxiedError', async () => {
let errorMessage = '';
try {
await guestMessagePipe.sendMessage('bad-handler', null);
} catch (error) {
errorMessage = error.message;
}
assertEquals(errorMessage, 'This is an error');
testDone();
});
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/** @type {!MessagePipe} */
var parentMessagePipe;
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment