messages: handle all subjects in top level
refs INTEROP-7086 flag=none why: * since all postMessages should be using subject and not messageType, handle them all in the same place * reduces complexity, since before some subjects were handled by a switch statement * prepare for further refactoring like adding lower level handler tests back, and renaming post_message folder * move all subjects to their own file * remove lower-level handler that used to do subjects by file, and move that logic to the top level * remove switch statement that used to handle "legacy" subjects, and add file for each subject from that test plan: * specs pass * sending each of these postMessage subjects works like they should Change-Id: Ia2b0552b6df895757581c5735f89d62158fa5a41 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/273933 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Mysti Lilla <mysti@instructure.com> QA-Review: Xander Moffatt <xmoffatt@instructure.com> Product-Review: Xander Moffatt <xmoffatt@instructure.com>
This commit is contained in:
parent
5438d8f86e
commit
2001fcb357
|
@ -35,10 +35,6 @@ const fetchWindowSize = {
|
|||
subject: 'lti.fetchWindowSize'
|
||||
}
|
||||
|
||||
const scrollMessage = {
|
||||
subject: 'lti.scrollToTop'
|
||||
}
|
||||
|
||||
const removeUnloadMessage = {
|
||||
subject: 'lti.removeUnloadMessage'
|
||||
}
|
||||
|
@ -81,7 +77,7 @@ QUnit.module('Messages', suiteHooks => {
|
|||
ltiToolWrapperFixture.empty()
|
||||
})
|
||||
|
||||
test('finds and resizes the tool content wrapper', () => {
|
||||
test('finds and resizes the tool content wrapper', async () => {
|
||||
ltiToolWrapperFixture.append(`
|
||||
<div id="content-wrapper" class="ic-Layout-contentWrapper">
|
||||
<div id="content" class="ic-Layout-contentMain" role="main">
|
||||
|
@ -95,11 +91,11 @@ QUnit.module('Messages', suiteHooks => {
|
|||
const toolContentWrapper = el.find('.tool_content_wrapper')
|
||||
|
||||
equal(toolContentWrapper.height(), 100)
|
||||
ltiMessageHandler(postMessageEvent(resizeMessage))
|
||||
await ltiMessageHandler(postMessageEvent(resizeMessage))
|
||||
equal(toolContentWrapper.height(), finalHeight)
|
||||
})
|
||||
|
||||
test('finds and resizes an iframe in embedded content', () => {
|
||||
test('finds and resizes an iframe in embedded content', async () => {
|
||||
ltiToolWrapperFixture.append(`
|
||||
<div>
|
||||
<h1 class="page-title">LTI resize test</h1>
|
||||
|
@ -109,11 +105,11 @@ QUnit.module('Messages', suiteHooks => {
|
|||
const iframe = $('iframe')
|
||||
|
||||
equal(iframe.height(), 100)
|
||||
ltiMessageHandler(postMessageEvent(resizeMessage, iframe[0].contentWindow))
|
||||
await ltiMessageHandler(postMessageEvent(resizeMessage, iframe[0].contentWindow))
|
||||
equal(iframe.height(), finalHeight)
|
||||
})
|
||||
|
||||
test('returns the hight and width of the page along with the iframe offset', () => {
|
||||
test('returns the height and width of the page along with the iframe offset', async () => {
|
||||
ltiToolWrapperFixture.append(`
|
||||
<div>
|
||||
<h1 class="page-title">LTI resize test</h1>
|
||||
|
@ -124,11 +120,11 @@ QUnit.module('Messages', suiteHooks => {
|
|||
|
||||
sinon.spy(iframe[0].contentWindow, 'postMessage')
|
||||
notOk(iframe[0].contentWindow.postMessage.calledOnce)
|
||||
ltiMessageHandler(postMessageEvent(fetchWindowSize, iframe[0].contentWindow))
|
||||
await ltiMessageHandler(postMessageEvent(fetchWindowSize, iframe[0].contentWindow))
|
||||
ok(iframe[0].contentWindow.postMessage.calledOnce)
|
||||
})
|
||||
|
||||
test('hides the module navigation', () => {
|
||||
test('hides the module navigation', async () => {
|
||||
ltiToolWrapperFixture.append(`
|
||||
<div>
|
||||
<div id="module-footer" class="module-sequence-footer">Next</div>
|
||||
|
@ -137,28 +133,28 @@ QUnit.module('Messages', suiteHooks => {
|
|||
const moduleFooter = $('#module-footer')
|
||||
|
||||
ok(moduleFooter.is(':visible'))
|
||||
ltiMessageHandler(postMessageEvent(showMessage(false)))
|
||||
await ltiMessageHandler(postMessageEvent(showMessage(false)))
|
||||
notOk(moduleFooter.is(':visible'))
|
||||
})
|
||||
|
||||
test('sets the unload message', () => {
|
||||
test('sets the unload message', async () => {
|
||||
sinon.spy(window, 'addEventListener')
|
||||
notOk(window.addEventListener.calledOnce)
|
||||
ltiMessageHandler(postMessageEvent(unloadMessage()))
|
||||
await ltiMessageHandler(postMessageEvent(unloadMessage()))
|
||||
ok(window.addEventListener.calledOnce)
|
||||
})
|
||||
|
||||
test('remove the unload message', () => {
|
||||
ltiMessageHandler(postMessageEvent(unloadMessage()))
|
||||
test('remove the unload message', async () => {
|
||||
await ltiMessageHandler(postMessageEvent(unloadMessage()))
|
||||
sinon.spy(window, 'removeEventListener')
|
||||
notOk(window.removeEventListener.calledOnce)
|
||||
ltiMessageHandler(postMessageEvent(removeUnloadMessage))
|
||||
await ltiMessageHandler(postMessageEvent(removeUnloadMessage))
|
||||
ok(window.removeEventListener.calledOnce)
|
||||
})
|
||||
|
||||
test('triggers a screen reader alert', () => {
|
||||
test('triggers a screen reader alert', async () => {
|
||||
sinon.spy($, 'screenReaderFlashMessageExclusive')
|
||||
ltiMessageHandler(postMessageEvent(alertMessage()))
|
||||
await ltiMessageHandler(postMessageEvent(alertMessage()))
|
||||
ok($.screenReaderFlashMessageExclusive.calledOnce)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
import {ltiMessageHandler} from '../messages'
|
||||
import $ from 'jquery'
|
||||
import $ from '@canvas/rails-flash-notifications'
|
||||
|
||||
describe('ltiMessageHander', () => {
|
||||
/* eslint-disable no-console */
|
||||
|
@ -39,30 +39,22 @@ describe('ltiMessageHander', () => {
|
|||
})
|
||||
/* eslint-enable no-console */
|
||||
|
||||
it('does not log unparseable messages from window.postMessage', () => {
|
||||
ltiMessageHandler({data: 'abcdef'})
|
||||
it('does not log unparseable messages from window.postMessage', async () => {
|
||||
await ltiMessageHandler({data: 'abcdef'})
|
||||
expect(logMock).not.toHaveBeenCalled()
|
||||
expect(errorMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not log ignored messages from window.postMessage', () => {
|
||||
ltiMessageHandler({data: JSON.stringify({a: 'b', c: 'd'})})
|
||||
ltiMessageHandler({data: {abc: 'def'}})
|
||||
it('does not log ignored messages from window.postMessage', async () => {
|
||||
await ltiMessageHandler({data: JSON.stringify({a: 'b', c: 'd'})})
|
||||
await ltiMessageHandler({data: {abc: 'def'}})
|
||||
expect(logMock).not.toHaveBeenCalled()
|
||||
expect(errorMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('handles parseable messages from window.postMessage', () => {
|
||||
it('handles parseable messages from window.postMessage', async () => {
|
||||
const flashMessage = jest.spyOn($, 'screenReaderFlashMessageExclusive')
|
||||
ltiMessageHandler({data: JSON.stringify({subject: 'lti.screenReaderAlert', body: 'Hi'})})
|
||||
await ltiMessageHandler({data: JSON.stringify({subject: 'lti.screenReaderAlert', body: 'Hi'})})
|
||||
expect(flashMessage).toHaveBeenCalledWith('Hi')
|
||||
})
|
||||
|
||||
it('prevents html from being passed to screenReaderFlashMessageExclusive', () => {
|
||||
const flashMessage = jest.spyOn($, 'screenReaderFlashMessageExclusive')
|
||||
ltiMessageHandler({
|
||||
data: JSON.stringify({subject: 'lti.screenReaderAlert', body: {html: 'abc'}})
|
||||
})
|
||||
expect(flashMessage).toHaveBeenCalledWith('{"html":"abc"}')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -18,28 +18,44 @@
|
|||
|
||||
/* eslint no-console: 0 */
|
||||
|
||||
import $ from 'jquery'
|
||||
import '@canvas/rails-flash-notifications'
|
||||
import htmlEscape from 'html-escape'
|
||||
import ToolLaunchResizer from './tool_launch_resizer'
|
||||
import handleLtiPostMessage from './post_message/handleLtiPostMessage'
|
||||
import {setUnloadMessage, removeUnloadMessage, findDomForWindow} from './util'
|
||||
import {findDomForWindow} from './util'
|
||||
import {
|
||||
NAVIGATION_MESSAGE as MENTIONS_NAVIGATION_MESSAGE,
|
||||
INPUT_CHANGE_MESSAGE as MENTIONS_INPUT_CHANGE_MESSAGE,
|
||||
SELECTION_MESSAGE as MENTIONS_SELECTION_MESSAGE
|
||||
} from '../../rce/plugins/canvas_mentions/constants'
|
||||
|
||||
// page-global storage for data relevant to LTI postMessage events
|
||||
const ltiState = {}
|
||||
export {ltiState}
|
||||
|
||||
export function ltiMessageHandler(e) {
|
||||
const SUBJECT_ALLOW_LIST = [
|
||||
'lti.enableScrollEvents',
|
||||
'lti.fetchWindowSize',
|
||||
'lti.frameResize',
|
||||
'lti.removeUnloadMessage',
|
||||
'lti.resourceImported',
|
||||
'lti.screenReaderAlert',
|
||||
'lti.scrollToTop',
|
||||
'lti.setUnloadMessage',
|
||||
'lti.showModuleNavigation',
|
||||
'requestFullWindowLaunch',
|
||||
'toggleCourseNavigationMenu'
|
||||
]
|
||||
|
||||
// These are handled elsewhere so ignore them
|
||||
const SUBJECT_IGNORE_LIST = [
|
||||
'A2ExternalContentReady',
|
||||
'LtiDeepLinkingResponse',
|
||||
MENTIONS_NAVIGATION_MESSAGE,
|
||||
MENTIONS_INPUT_CHANGE_MESSAGE,
|
||||
MENTIONS_SELECTION_MESSAGE
|
||||
]
|
||||
|
||||
async function ltiMessageHandler(e) {
|
||||
if (e.data.source && e.data.source.includes('react-devtools')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.data.messageType) {
|
||||
handleLtiPostMessage(e)
|
||||
return
|
||||
}
|
||||
|
||||
// Legacy post message handlers
|
||||
let message
|
||||
try {
|
||||
message = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
|
||||
|
@ -48,109 +64,26 @@ export function ltiMessageHandler(e) {
|
|||
return
|
||||
}
|
||||
|
||||
// look at messageType for backwards compatibility
|
||||
const subject = message.subject || message.messageType
|
||||
|
||||
if (SUBJECT_IGNORE_LIST.includes(subject) || !SUBJECT_ALLOW_LIST.includes(subject)) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
switch (message.subject) {
|
||||
case 'lti.frameResize': {
|
||||
const toolResizer = new ToolLaunchResizer()
|
||||
let height = message.height
|
||||
if (height <= 0) height = 1
|
||||
|
||||
const container = toolResizer
|
||||
.tool_content_wrapper(message.token || e.origin)
|
||||
.data('height_overridden', true)
|
||||
// If content.length is 0 then jquery didn't the tool wrapper.
|
||||
if (container.length > 0) {
|
||||
toolResizer.resize_tool_content_wrapper(height, container)
|
||||
} else {
|
||||
// Attempt to find an embedded iframe that matches the event source.
|
||||
const iframe = findDomForWindow(e.source)
|
||||
if (iframe) {
|
||||
if (typeof height === 'number') {
|
||||
height += 'px'
|
||||
}
|
||||
iframe.height = height
|
||||
iframe.style.height = height
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'lti.fetchWindowSize': {
|
||||
const iframe = findDomForWindow(e.source)
|
||||
if (iframe) {
|
||||
message.height = window.innerHeight
|
||||
message.width = window.innerWidth
|
||||
message.offset = $('.tool_content_wrapper').offset()
|
||||
message.footer = $('#fixed_bottom').height() || 0
|
||||
message.scrollY = window.scrollY
|
||||
const strMessage = JSON.stringify(message)
|
||||
|
||||
iframe.contentWindow.postMessage(strMessage, '*')
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'lti.showModuleNavigation':
|
||||
if (message.show === true || message.show === false) {
|
||||
$('.module-sequence-footer').toggle(message.show)
|
||||
}
|
||||
break
|
||||
|
||||
case 'lti.scrollToTop':
|
||||
$('html,body').animate(
|
||||
{
|
||||
scrollTop: $('.tool_content_wrapper').offset().top
|
||||
},
|
||||
'fast'
|
||||
)
|
||||
break
|
||||
|
||||
case 'lti.setUnloadMessage':
|
||||
setUnloadMessage(htmlEscape(message.message))
|
||||
break
|
||||
|
||||
case 'lti.removeUnloadMessage':
|
||||
removeUnloadMessage()
|
||||
break
|
||||
|
||||
case 'lti.screenReaderAlert':
|
||||
$.screenReaderFlashMessageExclusive(
|
||||
typeof message.body === 'string' ? message.body : JSON.stringify(message.body)
|
||||
)
|
||||
break
|
||||
|
||||
case 'lti.enableScrollEvents': {
|
||||
const iframe = findDomForWindow(e.source)
|
||||
if (iframe) {
|
||||
let timeout
|
||||
window.addEventListener(
|
||||
'scroll',
|
||||
() => {
|
||||
// requesting animation frames effectively debounces the scroll messages being sent
|
||||
if (timeout) {
|
||||
window.cancelAnimationFrame(timeout)
|
||||
}
|
||||
|
||||
timeout = window.requestAnimationFrame(() => {
|
||||
const msg = JSON.stringify({
|
||||
subject: 'lti.scroll',
|
||||
scrollY: window.scrollY
|
||||
})
|
||||
iframe.contentWindow.postMessage(msg, '*')
|
||||
})
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
;(console.error || console.log).call(console, 'invalid message received from')
|
||||
const handlerModule = await import(`./post_message/${subject}.js`)
|
||||
handlerModule.default({message, iframe: findDomForWindow(e.source), event: e})
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`Error loading or executing message handler for "${subject}"`, error)
|
||||
}
|
||||
}
|
||||
|
||||
export function monitorLtiMessages() {
|
||||
function monitorLtiMessages() {
|
||||
window.addEventListener('message', e => {
|
||||
if (e.data !== '') ltiMessageHandler(e)
|
||||
})
|
||||
}
|
||||
|
||||
export {ltiState, SUBJECT_ALLOW_LIST, SUBJECT_IGNORE_LIST, ltiMessageHandler, monitorLtiMessages}
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import handleLtiPostMessage from '../handleLtiPostMessage'
|
||||
import {ltiState} from '../../messages'
|
||||
|
||||
const requestFullWindowLaunchMessage = {
|
||||
messageType: 'requestFullWindowLaunch',
|
||||
data: 'http://localhost/test'
|
||||
}
|
||||
|
||||
const reactDevToolsBridge = {
|
||||
data: 'http://localhost/test',
|
||||
source: 'react-devtools-bridge'
|
||||
}
|
||||
|
||||
function postMessageEvent(data, origin, source) {
|
||||
return {
|
||||
data,
|
||||
origin,
|
||||
source
|
||||
}
|
||||
}
|
||||
|
||||
function invalidMessageTypeErrorCalls() {
|
||||
// eslint-disable-next-line no-console
|
||||
return console.error.mock.calls.filter(x => x.toString().includes('invalid messageType'))
|
||||
}
|
||||
|
||||
describe('handleLtiPostMessage', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'error')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error.mockRestore()
|
||||
})
|
||||
|
||||
describe('when a whitelisted event is processed', () => {
|
||||
it('attempts to call the message handler', async () => {
|
||||
ENV.context_asset_string = 'account_1'
|
||||
const wasCalled = await handleLtiPostMessage(postMessageEvent(requestFullWindowLaunchMessage))
|
||||
expect(wasCalled).toBeTruthy()
|
||||
expect(invalidMessageTypeErrorCalls().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a non-whitelisted event is processed', () => {
|
||||
it('does not error nor attempt to call the message handler', async () => {
|
||||
const wasCalled = await handleLtiPostMessage(postMessageEvent({messageType: 'notSupported'}))
|
||||
expect(wasCalled).toBeFalsy()
|
||||
expect(invalidMessageTypeErrorCalls().length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when an ignored event is processed', () => {
|
||||
it('does not attempt to call the message handler', async () => {
|
||||
const wasCalled = await handleLtiPostMessage(
|
||||
postMessageEvent({messageType: 'LtiDeepLinkingResponse'})
|
||||
)
|
||||
expect(wasCalled).toBeFalsy()
|
||||
expect(invalidMessageTypeErrorCalls().length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when source is react-dev-tools', () => {
|
||||
it('does not attempt to call the message handler', async () => {
|
||||
const wasCalled = await handleLtiPostMessage(postMessageEvent(reactDevToolsBridge))
|
||||
expect(wasCalled).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ltiState', () => {
|
||||
it('is empty initially', () => {
|
||||
expect(ltiState).toEqual({})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import handler from '../lti.screenReaderAlert'
|
||||
import $ from '@canvas/rails-flash-notifications'
|
||||
|
||||
describe('lti.screenReaderAlert handler', () => {
|
||||
it('prevents html from being passed to screenReaderFlashMessageExclusive', () => {
|
||||
const flashMessage = jest.spyOn($, 'screenReaderFlashMessageExclusive')
|
||||
handler({
|
||||
message: {body: {html: 'abc'}}
|
||||
})
|
||||
expect(flashMessage).toHaveBeenCalledWith('{"html":"abc"}')
|
||||
})
|
||||
})
|
|
@ -37,18 +37,18 @@ describe('requestFullWindowLaunch', () => {
|
|||
|
||||
describe('with string provided', () => {
|
||||
it('uses launch type same_window', () => {
|
||||
handler('http://localhost/test')
|
||||
handler({message: {data: 'http://localhost/test'}})
|
||||
expect(window.location.assign).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('pulls out client_id if provided', () => {
|
||||
handler('http://localhost/test?client_id=hello')
|
||||
handler({message: {data: 'http://localhost/test?client_id=hello'}})
|
||||
const launch_url = new URL(window.location.assign.mock.calls[0][0])
|
||||
expect(launch_url.searchParams.get('client_id')).toEqual('hello')
|
||||
})
|
||||
|
||||
it('pulls out assignment_id if provided', () => {
|
||||
handler('http://localhost/test?client_id=hello&assignment_id=50')
|
||||
handler({message: {data: 'http://localhost/test?client_id=hello&assignment_id=50'}})
|
||||
const launch_url = new URL(window.location.assign.mock.calls[0][0])
|
||||
expect(launch_url.searchParams.get('assignment_id')).toEqual('50')
|
||||
})
|
||||
|
@ -56,21 +56,23 @@ describe('requestFullWindowLaunch', () => {
|
|||
|
||||
describe('with object provided', () => {
|
||||
it('must contain a `url` property', () => {
|
||||
expect(() => handler({foo: 'bar'})).toThrow('message must contain a `url` property')
|
||||
expect(() => handler({message: {data: {foo: 'bar'}}})).toThrow(
|
||||
'message must contain a `url` property'
|
||||
)
|
||||
})
|
||||
|
||||
it('uses launch type same_window by default', () => {
|
||||
handler({url: 'http://localhost/test'})
|
||||
handler({message: {data: {url: 'http://localhost/test'}}})
|
||||
expect(window.location.assign).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens launch type new_window in a new tab', () => {
|
||||
handler({url: 'http://localhost/test', launchType: 'new_window'})
|
||||
handler({message: {data: {url: 'http://localhost/test', launchType: 'new_window'}}})
|
||||
expect(window.open).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens launch type popup in a popup window', () => {
|
||||
handler({url: 'http://localhost/test', launchType: 'popup'})
|
||||
handler({message: {data: {url: 'http://localhost/test', launchType: 'popup'}}})
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
'popupLaunch',
|
||||
|
@ -79,13 +81,13 @@ describe('requestFullWindowLaunch', () => {
|
|||
})
|
||||
|
||||
it('errors on unknown launch type', () => {
|
||||
expect(() => handler({url: 'http://localhost/test', launchType: 'fake'})).toThrow(
|
||||
"unknown launchType, must be 'popup', 'new_window', 'same_window'"
|
||||
)
|
||||
expect(() =>
|
||||
handler({message: {data: {url: 'http://localhost/test', launchType: 'fake'}}})
|
||||
).toThrow("unknown launchType, must be 'popup', 'new_window', 'same_window'")
|
||||
})
|
||||
|
||||
it('uses placement to add to launch url', () => {
|
||||
handler({url: 'http://localhost/test', placement: 'course_navigation'})
|
||||
handler({message: {data: {url: 'http://localhost/test', placement: 'course_navigation'}}})
|
||||
expect(window.location.assign).toHaveBeenCalledWith(
|
||||
expect.stringContaining('&placement=course_navigation')
|
||||
)
|
||||
|
@ -93,9 +95,13 @@ describe('requestFullWindowLaunch', () => {
|
|||
|
||||
it('uses launchOptions to add width and height to popup', () => {
|
||||
handler({
|
||||
message: {
|
||||
data: {
|
||||
url: 'http://localhost/test',
|
||||
launchType: 'popup',
|
||||
launchOptions: {width: 420, height: 400}
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
|
@ -105,13 +111,13 @@ describe('requestFullWindowLaunch', () => {
|
|||
})
|
||||
|
||||
it('uses display type borderless by default', () => {
|
||||
handler({url: 'http://localhost/test'})
|
||||
handler({message: {data: {url: 'http://localhost/test'}}})
|
||||
const launch_url = new URL(window.location.assign.mock.calls[0][0])
|
||||
expect(launch_url.searchParams.get('display')).toEqual('borderless')
|
||||
})
|
||||
|
||||
it('allows display type to be overridden', () => {
|
||||
handler({url: 'http://localhost/test', display: 'full_width_in_context'})
|
||||
handler({message: {data: {url: 'http://localhost/test', display: 'full_width_in_context'}}})
|
||||
const launch_url = new URL(window.location.assign.mock.calls[0][0])
|
||||
expect(launch_url.searchParams.get('display')).toEqual('full_width_in_context')
|
||||
})
|
||||
|
@ -119,7 +125,7 @@ describe('requestFullWindowLaunch', () => {
|
|||
|
||||
describe('with anything other than a string or object provided', () => {
|
||||
it('errors', () => {
|
||||
expect(() => handler(['foo', 'bar'])).toThrow(
|
||||
expect(() => handler({message: {data: ['foo', 'bar']}})).toThrow(
|
||||
'message contents must either be a string or an object'
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
NAVIGATION_MESSAGE as MENTIONS_NAVIGATION_MESSAGE,
|
||||
INPUT_CHANGE_MESSAGE as MENTIONS_INPUT_CHANGE_MESSAGE,
|
||||
SELECTION_MESSAGE as MENTIONS_SELECTION_MESSAGE
|
||||
} from '../../../rce/plugins/canvas_mentions/constants'
|
||||
|
||||
const SUBJECT_ALLOW_LIST = [
|
||||
'requestFullWindowLaunch',
|
||||
'lti.resourceImported',
|
||||
'toggleCourseNavigationMenu'
|
||||
]
|
||||
|
||||
// These are handled elsewhere so ignore them
|
||||
const SUBJECT_IGNORE_LIST = [
|
||||
'A2ExternalContentReady',
|
||||
'LtiDeepLinkingResponse',
|
||||
MENTIONS_NAVIGATION_MESSAGE,
|
||||
MENTIONS_INPUT_CHANGE_MESSAGE,
|
||||
MENTIONS_SELECTION_MESSAGE
|
||||
]
|
||||
|
||||
const handleLtiPostMessage = async e => {
|
||||
const {messageType, data} = e.data
|
||||
let handler
|
||||
|
||||
if (SUBJECT_IGNORE_LIST.includes(messageType)) {
|
||||
// These messages are handled elsewhere
|
||||
return false
|
||||
} else if (!SUBJECT_ALLOW_LIST.includes(messageType)) {
|
||||
// Enforce messageType allowlist -- unknown type
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`invalid messageType: ${messageType}`)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const handlerModule = await import(`./${messageType}.js`)
|
||||
handler = handlerModule.default
|
||||
handler(data)
|
||||
return true
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Error loading or executing message handler for "${messageType}"`, error)
|
||||
}
|
||||
}
|
||||
|
||||
export default handleLtiPostMessage
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export default function enableScrollEvents({iframe}) {
|
||||
if (iframe) {
|
||||
let timeout
|
||||
window.addEventListener(
|
||||
'scroll',
|
||||
() => {
|
||||
// requesting animation frames effectively debounces the scroll messages being sent
|
||||
if (timeout) {
|
||||
window.cancelAnimationFrame(timeout)
|
||||
}
|
||||
|
||||
timeout = window.requestAnimationFrame(() => {
|
||||
const msg = JSON.stringify({
|
||||
subject: 'lti.scroll',
|
||||
scrollY: window.scrollY
|
||||
})
|
||||
iframe.contentWindow.postMessage(msg, '*')
|
||||
})
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
|
||||
export default function fetchWindowSize({message, iframe}) {
|
||||
if (iframe) {
|
||||
message.height = window.innerHeight
|
||||
message.width = window.innerWidth
|
||||
message.offset = $('.tool_content_wrapper').offset()
|
||||
message.footer = $('#fixed_bottom').height() || 0
|
||||
message.scrollY = window.scrollY
|
||||
const strMessage = JSON.stringify(message)
|
||||
|
||||
iframe.contentWindow.postMessage(strMessage, '*')
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ToolLaunchResizer from '../tool_launch_resizer'
|
||||
|
||||
export default function frameResize({message, iframe, event}) {
|
||||
const toolResizer = new ToolLaunchResizer()
|
||||
let height = message.height
|
||||
if (height <= 0) height = 1
|
||||
|
||||
const container = toolResizer
|
||||
.tool_content_wrapper(message.token || event.origin)
|
||||
.data('height_overridden', true)
|
||||
// If content.length is 0 then jquery didn't the tool wrapper.
|
||||
if (container.length > 0) {
|
||||
toolResizer.resize_tool_content_wrapper(height, container)
|
||||
} else if (iframe) {
|
||||
// Attempt to find an embedded iframe that matches the event source.
|
||||
if (typeof height === 'number') {
|
||||
height += 'px'
|
||||
}
|
||||
iframe.height = height
|
||||
iframe.style.height = height
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {removeUnloadMessage} from '../util'
|
||||
|
||||
export default function remove() {
|
||||
removeUnloadMessage()
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {ltiState} from '../messages'
|
||||
|
||||
const handler = () => {
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import $ from '@canvas/rails-flash-notifications'
|
||||
|
||||
export default function screenReaderAlert({message}) {
|
||||
$.screenReaderFlashMessageExclusive(
|
||||
typeof message.body === 'string' ? message.body : JSON.stringify(message.body)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
|
||||
export default function scrollToTop() {
|
||||
$('html,body').animate(
|
||||
{
|
||||
scrollTop: $('.tool_content_wrapper').offset().top
|
||||
},
|
||||
'fast'
|
||||
)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import htmlEscape from 'html-escape'
|
||||
import {setUnloadMessage} from '../util'
|
||||
|
||||
export default function set({message}) {
|
||||
setUnloadMessage(htmlEscape(message.message))
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas 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 Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
|
||||
export default function showModuleNavigation({message}) {
|
||||
if (message.show === true || message.show === false) {
|
||||
$('.module-sequence-footer').toggle(message.show)
|
||||
}
|
||||
}
|
|
@ -64,8 +64,8 @@ const buildLaunchUrl = (messageUrl, placement, display) => {
|
|||
return `${baseUrl}&url=${encodedToolLaunchUrl}${clientIdParam}${placementParam}${assignmentParam}`
|
||||
}
|
||||
|
||||
const handler = data => {
|
||||
const {url, launchType, launchOptions, placement, display} = parseData(data)
|
||||
const handler = ({message}) => {
|
||||
const {url, launchType, launchOptions, placement, display} = parseData(message.data)
|
||||
const launchUrl = buildLaunchUrl(url, placement, display)
|
||||
|
||||
let proxy
|
||||
|
|
Loading…
Reference in New Issue