Convert postMessages to Typescript
fixes INTEROP-7910 Change-Id: I75cb94bb97acd03c1fe6a304d0506787cf7dd721 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/308290 QA-Review: Xander Moffatt <xmoffatt@instructure.com> Reviewed-by: Xander Moffatt <xmoffatt@instructure.com> Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Build-Review: Aaron Ogata <aogata@instructure.com> Product-Review: Paul Gray <paul.gray@instructure.com>
This commit is contained in:
parent
2882b35dcf
commit
70ba806b5c
|
@ -125,12 +125,6 @@
|
|||
"target": "ui/shared/datetime/changeTimezone.js",
|
||||
"request": "@canvas/datetime/changeTimezone"
|
||||
},
|
||||
{
|
||||
"name": "SpecifierMismatchError",
|
||||
"source": "ui/shared/lti/jquery/messages.js",
|
||||
"target": "ui/shared/rce/plugins/canvas_mentions/constants.js",
|
||||
"request": "../../rce/plugins/canvas_mentions/constants"
|
||||
},
|
||||
{
|
||||
"name": "SpecifierMismatchError",
|
||||
"source": "ui/shared/outcomes/react/hooks/useGroupCreate.js",
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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 {ResponseMessages} from './response_messages'
|
||||
|
||||
export interface LtiMessageHandler<T = unknown> {
|
||||
/**
|
||||
* /**
|
||||
* A handler for a single type of LTI postMessage message
|
||||
* @param params includes the message
|
||||
* @returns true if the handler has already sent a response
|
||||
*/
|
||||
(params: {message: T; event: MessageEvent<unknown>; responseMessages: ResponseMessages}): boolean
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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/>.
|
||||
*/
|
||||
|
||||
/* eslint no-console: 0 */
|
||||
|
||||
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'
|
||||
import buildResponseMessages from './response_messages'
|
||||
|
||||
// page-global storage for data relevant to LTI postMessage events
|
||||
const ltiState = {}
|
||||
|
||||
const SUBJECT_ALLOW_LIST = [
|
||||
'lti.enableScrollEvents',
|
||||
'lti.fetchWindowSize',
|
||||
'lti.frameResize',
|
||||
'lti.hideRightSideWrapper',
|
||||
'lti.removeUnloadMessage',
|
||||
'lti.resourceImported',
|
||||
'lti.screenReaderAlert',
|
||||
'lti.scrollToTop',
|
||||
'lti.setUnloadMessage',
|
||||
'lti.showAlert',
|
||||
'lti.showModuleNavigation',
|
||||
'org.imsglobal.lti.capabilities',
|
||||
'org.imsglobal.lti.get_data',
|
||||
'org.imsglobal.lti.put_data',
|
||||
'requestFullWindowLaunch',
|
||||
'toggleCourseNavigationMenu',
|
||||
]
|
||||
|
||||
// These are handled elsewhere so ignore them
|
||||
const SUBJECT_IGNORE_LIST = [
|
||||
'A2ExternalContentReady',
|
||||
'LtiDeepLinkingResponse',
|
||||
'externalContentReady',
|
||||
'externalContentCancel',
|
||||
MENTIONS_NAVIGATION_MESSAGE,
|
||||
MENTIONS_INPUT_CHANGE_MESSAGE,
|
||||
MENTIONS_SELECTION_MESSAGE,
|
||||
'betterchat.is_mini_chat',
|
||||
]
|
||||
|
||||
async function ltiMessageHandler(e) {
|
||||
if (e.data?.source?.includes('react-devtools') || e.data.isAngularDevTools) {
|
||||
return false
|
||||
}
|
||||
|
||||
let message
|
||||
try {
|
||||
message = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
|
||||
} catch (err) {
|
||||
// unparseable message may not be meant for our handlers
|
||||
return false
|
||||
}
|
||||
|
||||
// look at messageType for backwards compatibility
|
||||
const subject = message.subject || message.messageType
|
||||
const responseMessages = buildResponseMessages({
|
||||
targetWindow: e.source,
|
||||
origin: e.origin,
|
||||
subject,
|
||||
message_id: message.message_id,
|
||||
toolOrigin: message.toolOrigin,
|
||||
})
|
||||
|
||||
if (
|
||||
SUBJECT_IGNORE_LIST.includes(subject) ||
|
||||
subject === undefined ||
|
||||
responseMessages.isResponse(e)
|
||||
) {
|
||||
// These messages are handled elsewhere
|
||||
return false
|
||||
} else if (!SUBJECT_ALLOW_LIST.includes(subject)) {
|
||||
// Enforce subject allowlist -- unknown type
|
||||
responseMessages.sendUnsupportedSubjectError()
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const handlerModule = await import(
|
||||
/* webpackExclude: /__tests__/ */
|
||||
`./subjects/${subject}.js`
|
||||
)
|
||||
const hasSentResponse = handlerModule.default({
|
||||
message,
|
||||
event: e,
|
||||
responseMessages,
|
||||
})
|
||||
if (!hasSentResponse) {
|
||||
responseMessages.sendSuccess()
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`Error loading or executing message handler for "${subject}": ${error}`)
|
||||
responseMessages.sendGenericError(error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let hasListener = false
|
||||
|
||||
function monitorLtiMessages() {
|
||||
const cb = e => {
|
||||
if (e.data !== '') ltiMessageHandler(e)
|
||||
}
|
||||
if (!hasListener) {
|
||||
window.addEventListener('message', cb)
|
||||
hasListener = true
|
||||
}
|
||||
}
|
||||
|
||||
export {ltiState, SUBJECT_ALLOW_LIST, SUBJECT_IGNORE_LIST, ltiMessageHandler, monitorLtiMessages}
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Copyright (C) 2018 - 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/>.
|
||||
*/
|
||||
|
||||
/* eslint no-console: 0 */
|
||||
|
||||
import {
|
||||
NAVIGATION_MESSAGE as MENTIONS_NAVIGATION_MESSAGE,
|
||||
INPUT_CHANGE_MESSAGE as MENTIONS_INPUT_CHANGE_MESSAGE,
|
||||
SELECTION_MESSAGE as MENTIONS_SELECTION_MESSAGE,
|
||||
} from '@canvas/rce/plugins/canvas_mentions/constants'
|
||||
import {LtiMessageHandler} from './lti_message_handler'
|
||||
import buildResponseMessages from './response_messages'
|
||||
import {getKey, hasKey} from './util'
|
||||
|
||||
// page-global storage for data relevant to LTI postMessage events
|
||||
const ltiState: {
|
||||
tray?: {refreshOnClose?: boolean}
|
||||
fullWindowProxy?: Window | null
|
||||
} = {}
|
||||
|
||||
const SUBJECT_ALLOW_LIST = [
|
||||
'lti.enableScrollEvents',
|
||||
'lti.fetchWindowSize',
|
||||
'lti.frameResize',
|
||||
'lti.hideRightSideWrapper',
|
||||
'lti.removeUnloadMessage',
|
||||
'lti.resourceImported',
|
||||
'lti.screenReaderAlert',
|
||||
'lti.scrollToTop',
|
||||
'lti.setUnloadMessage',
|
||||
'lti.showAlert',
|
||||
'lti.showModuleNavigation',
|
||||
'org.imsglobal.lti.capabilities',
|
||||
'org.imsglobal.lti.get_data',
|
||||
'org.imsglobal.lti.put_data',
|
||||
'requestFullWindowLaunch',
|
||||
'toggleCourseNavigationMenu',
|
||||
] as const
|
||||
|
||||
type SubjectId = typeof SUBJECT_ALLOW_LIST[number]
|
||||
|
||||
const isAllowedSubject = (subject: unknown): subject is SubjectId =>
|
||||
typeof subject === 'string' && (SUBJECT_ALLOW_LIST as ReadonlyArray<string>).includes(subject)
|
||||
|
||||
const isIgnoredSubject = (subject: unknown): subject is SubjectId =>
|
||||
typeof subject === 'string' && (SUBJECT_IGNORE_LIST as ReadonlyArray<string>).includes(subject)
|
||||
|
||||
// These are handled elsewhere so ignore them
|
||||
const SUBJECT_IGNORE_LIST = [
|
||||
'A2ExternalContentReady',
|
||||
'LtiDeepLinkingResponse',
|
||||
'externalContentReady',
|
||||
'externalContentCancel',
|
||||
MENTIONS_NAVIGATION_MESSAGE,
|
||||
MENTIONS_INPUT_CHANGE_MESSAGE,
|
||||
MENTIONS_SELECTION_MESSAGE,
|
||||
'betterchat.is_mini_chat',
|
||||
] as const
|
||||
|
||||
const isObject = (u: unknown): u is object => {
|
||||
return typeof u === 'object'
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the data from a message event is associated with a dev tool
|
||||
* @param data - The `data` attribute from a message event
|
||||
*/
|
||||
const isDevtoolMessageData = (data: unknown): boolean => {
|
||||
return (
|
||||
isObject(data) &&
|
||||
((hasKey('source', data) &&
|
||||
typeof data.source === 'string' &&
|
||||
data.source.includes('react-devtools')) ||
|
||||
(hasKey('isAngularDevTools', data) && !!data.isAngularDevTools))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A mapping of lti message id to a function that "handles" the message
|
||||
* The values are functions to preserve the asynchronous loading of
|
||||
* code that was present in the previous style. It may not be necessary.
|
||||
*/
|
||||
const handlers: Record<
|
||||
typeof SUBJECT_ALLOW_LIST[number],
|
||||
() => Promise<{default: LtiMessageHandler<any>}>
|
||||
> = {
|
||||
'lti.enableScrollEvents': () => import(`./subjects/lti.enableScrollEvents`),
|
||||
'lti.fetchWindowSize': () => import(`./subjects/lti.fetchWindowSize`),
|
||||
'lti.frameResize': () => import(`./subjects/lti.frameResize`),
|
||||
'lti.hideRightSideWrapper': () => import(`./subjects/lti.hideRightSideWrapper`),
|
||||
'lti.removeUnloadMessage': () => import(`./subjects/lti.removeUnloadMessage`),
|
||||
'lti.resourceImported': () => import(`./subjects/lti.resourceImported`),
|
||||
'lti.screenReaderAlert': () => import(`./subjects/lti.screenReaderAlert`),
|
||||
'lti.scrollToTop': () => import(`./subjects/lti.scrollToTop`),
|
||||
'lti.setUnloadMessage': () => import(`./subjects/lti.setUnloadMessage`),
|
||||
'lti.showAlert': () => import(`./subjects/lti.showAlert`),
|
||||
'lti.showModuleNavigation': () => import(`./subjects/lti.showModuleNavigation`),
|
||||
'org.imsglobal.lti.capabilities': () => import(`./subjects/org.imsglobal.lti.capabilities`),
|
||||
'org.imsglobal.lti.get_data': () => import(`./subjects/org.imsglobal.lti.get_data`),
|
||||
'org.imsglobal.lti.put_data': () => import(`./subjects/org.imsglobal.lti.put_data`),
|
||||
requestFullWindowLaunch: () => import(`./subjects/requestFullWindowLaunch`),
|
||||
toggleCourseNavigationMenu: () => import(`./subjects/toggleCourseNavigationMenu`),
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles 'message' events for LTI-related messages from LTI tools
|
||||
* @param e
|
||||
* @returns
|
||||
*/
|
||||
async function ltiMessageHandler(e: MessageEvent<unknown>) {
|
||||
if (isDevtoolMessageData(e.data)) {
|
||||
return false
|
||||
}
|
||||
|
||||
let message: unknown
|
||||
try {
|
||||
message = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
|
||||
} catch (err) {
|
||||
// unparseable message may not be meant for our handlers
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof message !== 'object' || message === null) {
|
||||
// unparseable message may not be meant for our handlers
|
||||
return false
|
||||
}
|
||||
|
||||
const targetWindow = e.source as Window
|
||||
|
||||
// look at messageType for backwards compatibility
|
||||
const subject = getKey('subject', message) || getKey('messageType', message)
|
||||
const responseMessages = buildResponseMessages({
|
||||
targetWindow,
|
||||
origin: e.origin,
|
||||
subject,
|
||||
message_id: getKey('message_id', message),
|
||||
toolOrigin: getKey('toolOrigin', message),
|
||||
})
|
||||
|
||||
if (subject === undefined || isIgnoredSubject(subject) || responseMessages.isResponse(e)) {
|
||||
// These messages are handled elsewhere
|
||||
return false
|
||||
} else if (!isAllowedSubject(subject)) {
|
||||
responseMessages.sendUnsupportedSubjectError()
|
||||
return false
|
||||
} else {
|
||||
try {
|
||||
const handlerModule = await handlers[subject]()
|
||||
const hasSentResponse = handlerModule.default({
|
||||
message,
|
||||
event: e,
|
||||
responseMessages,
|
||||
})
|
||||
if (!hasSentResponse) {
|
||||
responseMessages.sendSuccess()
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`Error loading or executing message handler for "${subject}": ${error}`)
|
||||
|
||||
const message =
|
||||
isObject(error) && hasKey('message', error) && typeof error.message === 'string'
|
||||
? error.message
|
||||
: undefined
|
||||
responseMessages.sendGenericError(message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let hasListener = false
|
||||
|
||||
function monitorLtiMessages() {
|
||||
const cb = (e: MessageEvent<unknown>) => {
|
||||
if (e.data !== '') ltiMessageHandler(e)
|
||||
}
|
||||
if (!hasListener) {
|
||||
window.addEventListener('message', cb)
|
||||
hasListener = true
|
||||
}
|
||||
}
|
||||
|
||||
export {ltiState, SUBJECT_ALLOW_LIST, SUBJECT_IGNORE_LIST, ltiMessageHandler, monitorLtiMessages}
|
|
@ -32,22 +32,22 @@ const getLimit = tool_id => {
|
|||
return limits[tool_id]
|
||||
}
|
||||
|
||||
const clearLimit = tool_id => {
|
||||
const clearLimit = (tool_id: string) => {
|
||||
delete limits[tool_id]
|
||||
}
|
||||
|
||||
const addToLimit = (tool_id, key, value) => {
|
||||
const addToLimit = (tool_id: string, key: string, value: string) => {
|
||||
createLimit(tool_id)
|
||||
const length = key.length + value.length
|
||||
|
||||
if (limits[tool_id].keyCount >= STORAGE_KEY_LIMIT) {
|
||||
const e = new Error('Reached key limit for tool')
|
||||
const e: Error & {code?: string} = new Error('Reached key limit for tool')
|
||||
e.code = 'storage_exhaustion'
|
||||
throw e
|
||||
}
|
||||
|
||||
if (limits[tool_id].charCount + length > STORAGE_CHAR_LIMIT) {
|
||||
const e = new Error('Reached byte limit for tool')
|
||||
const e: Error & {code?: string} = new Error('Reached byte limit for tool')
|
||||
e.code = 'storage_exhaustion'
|
||||
throw e
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ const addToLimit = (tool_id, key, value) => {
|
|||
limits[tool_id].charCount += length
|
||||
}
|
||||
|
||||
const removeFromLimit = (tool_id, key, value) => {
|
||||
const removeFromLimit = (tool_id: string, key: string, value: string) => {
|
||||
limits[tool_id].keyCount--
|
||||
limits[tool_id].charCount -= key.length + value.length
|
||||
|
||||
|
@ -68,16 +68,16 @@ const removeFromLimit = (tool_id, key, value) => {
|
|||
}
|
||||
}
|
||||
|
||||
const getKey = (tool_id, key) => `lti|platform_storage|${tool_id}|${key}`
|
||||
const getKey = (tool_id: string, key: string) => `lti|platform_storage|${tool_id}|${key}`
|
||||
|
||||
const putData = (tool_id, key, value) => {
|
||||
const putData = (tool_id: string, key: string, value: string) => {
|
||||
addToLimit(tool_id, key, value)
|
||||
window.localStorage.setItem(getKey(tool_id, key), value)
|
||||
}
|
||||
|
||||
const getData = (tool_id, key) => window.localStorage.getItem(getKey(tool_id, key))
|
||||
const getData = (tool_id: string, key: string) => window.localStorage.getItem(getKey(tool_id, key))
|
||||
|
||||
const clearData = (tool_id, key) => {
|
||||
const clearData = (tool_id: string, key: string) => {
|
||||
const value = getData(tool_id, key)
|
||||
if (value) {
|
||||
removeFromLimit(tool_id, key, value)
|
|
@ -21,9 +21,36 @@ const UNSUPPORTED_SUBJECT_ERROR_CODE = 'unsupported_subject'
|
|||
const WRONG_ORIGIN_ERROR_CODE = 'wrong_origin'
|
||||
const BAD_REQUEST_ERROR_CODE = 'bad_request'
|
||||
|
||||
const buildResponseMessages = ({targetWindow, origin, subject, message_id, toolOrigin}) => {
|
||||
export interface ResponseMessages {
|
||||
sendResponse: (contents?: {}) => void
|
||||
sendSuccess: () => void
|
||||
sendError: (code: string, message?: string | undefined) => void
|
||||
sendGenericError: (message?: string | undefined) => void
|
||||
sendBadRequestError: (message: any) => void
|
||||
sendWrongOriginError: () => void
|
||||
sendUnsupportedSubjectError: () => void
|
||||
isResponse: (message: any) => boolean
|
||||
}
|
||||
|
||||
const buildResponseMessages = ({
|
||||
targetWindow,
|
||||
origin,
|
||||
subject,
|
||||
message_id,
|
||||
toolOrigin,
|
||||
}: {
|
||||
targetWindow: Window | null
|
||||
origin: string
|
||||
subject: unknown
|
||||
message_id: unknown
|
||||
toolOrigin: unknown
|
||||
}): ResponseMessages => {
|
||||
const sendResponse = (contents = {}) => {
|
||||
const message = {subject: `${subject}.response`}
|
||||
const message: {
|
||||
subject: string
|
||||
message_id?: unknown
|
||||
toolOrigin?: unknown
|
||||
} = {subject: `${subject}.response`}
|
||||
if (message_id) {
|
||||
message.message_id = message_id
|
||||
}
|
||||
|
@ -42,15 +69,15 @@ const buildResponseMessages = ({targetWindow, origin, subject, message_id, toolO
|
|||
sendResponse({})
|
||||
}
|
||||
|
||||
const sendError = (code, message) => {
|
||||
const error = {code}
|
||||
const sendError = (code: string, message?: string) => {
|
||||
const error: {code: string; message?: string} = {code}
|
||||
if (message) {
|
||||
error.message = message
|
||||
}
|
||||
sendResponse({error})
|
||||
}
|
||||
|
||||
const sendGenericError = message => {
|
||||
const sendGenericError = (message?: string) => {
|
||||
sendError(GENERIC_ERROR_CODE, message)
|
||||
}
|
||||
|
|
@ -16,7 +16,9 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export default function enableScrollEvents({responseMessages}) {
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
|
||||
const enableScrollEvents: LtiMessageHandler = ({responseMessages}) => {
|
||||
let timeout
|
||||
window.addEventListener(
|
||||
'scroll',
|
||||
|
@ -37,3 +39,5 @@ export default function enableScrollEvents({responseMessages}) {
|
|||
)
|
||||
return true
|
||||
}
|
||||
|
||||
export default enableScrollEvents
|
|
@ -17,8 +17,9 @@
|
|||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
|
||||
export default function fetchWindowSize({responseMessages}) {
|
||||
const fetchWindowSize: LtiMessageHandler = ({responseMessages}) => {
|
||||
responseMessages.sendResponse({
|
||||
height: window.innerHeight,
|
||||
width: window.innerWidth,
|
||||
|
@ -28,3 +29,5 @@ export default function fetchWindowSize({responseMessages}) {
|
|||
})
|
||||
return true
|
||||
}
|
||||
|
||||
export default fetchWindowSize
|
|
@ -18,10 +18,14 @@
|
|||
|
||||
import ToolLaunchResizer from '../tool_launch_resizer'
|
||||
import {findDomForWindow} from '../util'
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
|
||||
export default function frameResize({message, event}) {
|
||||
const frameResize: LtiMessageHandler<{height: number | string; token: string}> = ({
|
||||
message,
|
||||
event,
|
||||
}) => {
|
||||
const toolResizer = new ToolLaunchResizer()
|
||||
let height = message.height
|
||||
let height: number | string = message.height as number | string
|
||||
if (height <= 0) height = 1
|
||||
|
||||
const container = toolResizer
|
||||
|
@ -34,11 +38,12 @@ export default function frameResize({message, event}) {
|
|||
// Attempt to find an embedded iframe that matches the event source.
|
||||
const iframe = findDomForWindow(event.source)
|
||||
if (iframe) {
|
||||
if (typeof height === 'number') {
|
||||
height += 'px'
|
||||
}
|
||||
iframe.height = height
|
||||
iframe.style.height = height
|
||||
const strHeight = typeof height === 'number' ? `${height}px` : height
|
||||
iframe.height = strHeight
|
||||
iframe.style.height = strHeight
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export default frameResize
|
|
@ -17,7 +17,11 @@
|
|||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
|
||||
export default function hideRightSideWrapper() {
|
||||
const hideRightSideWrapper: LtiMessageHandler = () => {
|
||||
$('#right-side-wrapper').hide()
|
||||
return false
|
||||
}
|
||||
|
||||
export default hideRightSideWrapper
|
|
@ -17,7 +17,11 @@
|
|||
*/
|
||||
|
||||
import {removeUnloadMessage} from '../util'
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
|
||||
export default function remove() {
|
||||
const remove: LtiMessageHandler = () => {
|
||||
removeUnloadMessage()
|
||||
return false
|
||||
}
|
||||
|
||||
export default remove
|
|
@ -16,13 +16,15 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
import {ltiState} from '../messages'
|
||||
|
||||
const handler = () => {
|
||||
const handler: LtiMessageHandler = () => {
|
||||
if (!ltiState.tray) {
|
||||
ltiState.tray = {}
|
||||
}
|
||||
ltiState.tray.refreshOnClose = true
|
||||
return false
|
||||
}
|
||||
|
||||
export default handler
|
|
@ -17,9 +17,15 @@
|
|||
*/
|
||||
|
||||
import $ from '@canvas/rails-flash-notifications'
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
|
||||
export default function screenReaderAlert({message}) {
|
||||
const screenReaderAlert: LtiMessageHandler<{
|
||||
body: string | unknown
|
||||
}> = ({message}) => {
|
||||
$.screenReaderFlashMessageExclusive(
|
||||
typeof message.body === 'string' ? message.body : JSON.stringify(message.body)
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
export default screenReaderAlert
|
|
@ -17,12 +17,16 @@
|
|||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
|
||||
export default function scrollToTop() {
|
||||
const scrollToTop: LtiMessageHandler = () => {
|
||||
$('html,body').animate(
|
||||
{
|
||||
scrollTop: $('.tool_content_wrapper').offset().top,
|
||||
scrollTop: $('.tool_content_wrapper').offset()?.top,
|
||||
},
|
||||
'fast'
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
export default scrollToTop
|
|
@ -18,7 +18,11 @@
|
|||
|
||||
import htmlEscape from 'html-escape'
|
||||
import {setUnloadMessage} from '../util'
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
|
||||
export default function set({message}) {
|
||||
const set: LtiMessageHandler<{message: string}> = ({message}) => {
|
||||
setUnloadMessage(htmlEscape(message.message))
|
||||
return false
|
||||
}
|
||||
|
||||
export default set
|
|
@ -18,10 +18,15 @@
|
|||
|
||||
import $ from '@canvas/rails-flash-notifications'
|
||||
import {useScope as useI18nScope} from '@canvas/i18n'
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
|
||||
const I18n = useI18nScope('ltiMessages')
|
||||
|
||||
export default function showAlert({message, responseMessages}) {
|
||||
const showAlert: LtiMessageHandler<{
|
||||
body: unknown
|
||||
title?: string
|
||||
alertType?: string
|
||||
}> = ({message, responseMessages}) => {
|
||||
if (!message.body) {
|
||||
responseMessages.sendBadRequestError("Missing required 'body' field")
|
||||
return true
|
||||
|
@ -49,3 +54,5 @@ export default function showAlert({message, responseMessages}) {
|
|||
responseMessages.sendSuccess()
|
||||
return true
|
||||
}
|
||||
|
||||
export default showAlert
|
|
@ -17,9 +17,15 @@
|
|||
*/
|
||||
|
||||
import $ from 'jquery'
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
|
||||
export default function showModuleNavigation({message}) {
|
||||
const showModuleNavigation: LtiMessageHandler<{
|
||||
show?: boolean
|
||||
}> = ({message}) => {
|
||||
if (message.show === true || message.show === false) {
|
||||
$('.module-sequence-footer').toggle(message.show)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export default showModuleNavigation
|
|
@ -16,9 +16,10 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
import {SUBJECT_ALLOW_LIST} from '../messages'
|
||||
|
||||
export default ({responseMessages}) => {
|
||||
const handler: LtiMessageHandler<unknown> = ({responseMessages}) => {
|
||||
const useFrame = ENV?.FEATURES?.lti_platform_storage
|
||||
const imsSubjects = ['org.imsglobal.lti.get_data', 'org.imsglobal.lti.put_data']
|
||||
const supported_messages = SUBJECT_ALLOW_LIST.map(subject => {
|
||||
|
@ -34,3 +35,5 @@ export default ({responseMessages}) => {
|
|||
responseMessages.sendResponse({supported_messages})
|
||||
return true
|
||||
}
|
||||
|
||||
export default handler
|
|
@ -17,8 +17,12 @@
|
|||
*/
|
||||
|
||||
import {getData} from '../platform_storage'
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
|
||||
export default function handler({message, responseMessages, event}) {
|
||||
const handler: LtiMessageHandler<{
|
||||
key: string
|
||||
message_id: string
|
||||
}> = ({message, responseMessages, event}) => {
|
||||
const {key, message_id} = message
|
||||
|
||||
if (!key) {
|
||||
|
@ -35,3 +39,5 @@ export default function handler({message, responseMessages, event}) {
|
|||
responseMessages.sendResponse({key, value})
|
||||
return true
|
||||
}
|
||||
|
||||
export default handler
|
|
@ -17,8 +17,14 @@
|
|||
*/
|
||||
|
||||
import {clearData, putData} from '../platform_storage'
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
import {getKey} from '../util'
|
||||
|
||||
export default function handler({message, responseMessages, event}) {
|
||||
const handler: LtiMessageHandler<{
|
||||
key: string
|
||||
value: string
|
||||
message_id: string
|
||||
}> = ({message, responseMessages, event}) => {
|
||||
const {key, value, message_id} = message
|
||||
|
||||
if (!key) {
|
||||
|
@ -35,8 +41,13 @@ export default function handler({message, responseMessages, event}) {
|
|||
try {
|
||||
putData(event.origin, key, value)
|
||||
responseMessages.sendResponse({key, value})
|
||||
} catch (e) {
|
||||
responseMessages.sendError(e.code, e.message)
|
||||
} catch (e: unknown) {
|
||||
const code = getKey('code', e)
|
||||
const message = getKey('message', e)
|
||||
responseMessages.sendError(
|
||||
typeof code === 'string' ? code : '',
|
||||
typeof message === 'string' ? message : undefined
|
||||
)
|
||||
}
|
||||
} else {
|
||||
clearData(event.origin, key)
|
||||
|
@ -44,3 +55,5 @@ export default function handler({message, responseMessages, event}) {
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export default handler
|
|
@ -15,9 +15,33 @@
|
|||
* 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 {LtiMessageHandler} from '../lti_message_handler'
|
||||
import {ltiState} from '../messages'
|
||||
import {hasKey} from '../util'
|
||||
|
||||
const parseData = data => {
|
||||
const parseData = (
|
||||
data:
|
||||
| {
|
||||
url?: string
|
||||
display?: string
|
||||
launchType?: string
|
||||
launchOptions?: {
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
placement?: string
|
||||
}
|
||||
| string
|
||||
): {
|
||||
url: string
|
||||
display: string
|
||||
launchType: string
|
||||
launchOptions: {
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
placement?: string
|
||||
} => {
|
||||
const defaults = {
|
||||
display: 'borderless',
|
||||
launchType: 'same_window',
|
||||
|
@ -28,20 +52,24 @@ const parseData = data => {
|
|||
url: data,
|
||||
...defaults,
|
||||
}
|
||||
} else if (typeof data === 'object' && !(data instanceof Array)) {
|
||||
if (!data.url) {
|
||||
throw new Error('message must contain a `url` property')
|
||||
}
|
||||
return {
|
||||
...defaults,
|
||||
...data,
|
||||
} else if (typeof data === 'object' && !(data instanceof Array) && data !== null) {
|
||||
if (hasKey('url', data)) {
|
||||
const url = data.url
|
||||
if (typeof url === 'string') {
|
||||
return {
|
||||
...defaults,
|
||||
...data,
|
||||
url,
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error('message must contain a `url` property')
|
||||
} else {
|
||||
throw new Error('message contents must either be a string or an object')
|
||||
}
|
||||
}
|
||||
|
||||
const buildLaunchUrl = (messageUrl, placement, display) => {
|
||||
const buildLaunchUrl = (messageUrl: string, placement: string | undefined, display: string) => {
|
||||
let context = ENV.context_asset_string.replace('_', 's/')
|
||||
if (!(context.startsWith('account') || context.startsWith('course'))) {
|
||||
context = 'accounts/' + ENV.DOMAIN_ROOT_ACCOUNT_ID
|
||||
|
@ -57,18 +85,29 @@ const buildLaunchUrl = (messageUrl, placement, display) => {
|
|||
const assignmentParam = assignmentId ? `&assignment_id=${assignmentId}` : ''
|
||||
|
||||
// xsslint safeString.property window.location
|
||||
toolLaunchUrl.searchParams.append('platform_redirect_url', window.location)
|
||||
toolLaunchUrl.searchParams.append('platform_redirect_url', window.location.toString())
|
||||
toolLaunchUrl.searchParams.append('full_win_launch_requested', '1')
|
||||
const encodedToolLaunchUrl = encodeURIComponent(toolLaunchUrl.toString())
|
||||
|
||||
return `${baseUrl}&url=${encodedToolLaunchUrl}${clientIdParam}${placementParam}${assignmentParam}`
|
||||
}
|
||||
|
||||
const handler = ({message}) => {
|
||||
const handler: LtiMessageHandler<{
|
||||
data: {
|
||||
url: string
|
||||
launchType: string
|
||||
launchOptions: {
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
placement: string
|
||||
display: string
|
||||
}
|
||||
}> = ({message}) => {
|
||||
const {url, launchType, launchOptions, placement, display} = parseData(message.data)
|
||||
const launchUrl = buildLaunchUrl(url, placement, display)
|
||||
|
||||
let proxy
|
||||
let proxy: Window | null = null
|
||||
switch (launchType) {
|
||||
case 'popup': {
|
||||
const width = launchOptions.width || 800
|
||||
|
@ -95,6 +134,7 @@ const handler = ({message}) => {
|
|||
|
||||
// keep a reference to close later
|
||||
ltiState.fullWindowProxy = proxy
|
||||
return false
|
||||
}
|
||||
|
||||
export default handler
|
|
@ -17,7 +17,11 @@
|
|||
*/
|
||||
|
||||
import {toggleCourseNav} from '@canvas/courses/jquery/toggleCourseNav'
|
||||
import {LtiMessageHandler} from '../lti_message_handler'
|
||||
|
||||
const handler = () => toggleCourseNav()
|
||||
const handler: LtiMessageHandler = () => {
|
||||
toggleCourseNav()
|
||||
return false
|
||||
}
|
||||
|
||||
export default handler
|
|
@ -19,15 +19,17 @@
|
|||
import $ from 'jquery'
|
||||
|
||||
export default class ToolLaunchResizer {
|
||||
constructor(minToolHeight) {
|
||||
minToolHeight: number
|
||||
|
||||
constructor(minToolHeight?: number) {
|
||||
this.minToolHeight = minToolHeight || 450
|
||||
}
|
||||
|
||||
sanitizedWrapperId(wrapperId) {
|
||||
sanitizedWrapperId(wrapperId?: string) {
|
||||
return wrapperId?.toString()?.replace(/[^a-zA-Z0-9_-]/g, '')
|
||||
}
|
||||
|
||||
tool_content_wrapper(wrapperId) {
|
||||
tool_content_wrapper(wrapperId?: string) {
|
||||
let container = $(`div[data-tool-wrapper-id*='${this.sanitizedWrapperId(wrapperId)}']`)
|
||||
const tool_content_wrapper = $('.tool_content_wrapper')
|
||||
if (container.length <= 0 && tool_content_wrapper.length === 1) {
|
||||
|
@ -36,7 +38,15 @@ export default class ToolLaunchResizer {
|
|||
return container
|
||||
}
|
||||
|
||||
resize_tool_content_wrapper(height, container, force_height = false) {
|
||||
resize_tool_content_wrapper(
|
||||
height: number | string,
|
||||
|
||||
// disabling b/c eslint fails, saying 'MessageEventSource' is not defined, but it's
|
||||
// defined in lib.dom.d.ts
|
||||
// eslint-disable-next-line no-undef
|
||||
container: JQuery<HTMLElement>,
|
||||
force_height = false
|
||||
) {
|
||||
let setHeight = height
|
||||
if (typeof setHeight !== 'number') {
|
||||
setHeight = this.minToolHeight
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
let beforeUnloadHandler
|
||||
function setUnloadMessage(msg) {
|
||||
removeUnloadMessage()
|
||||
|
||||
beforeUnloadHandler = function (e) {
|
||||
return (e.returnValue = msg || '')
|
||||
}
|
||||
window.addEventListener('beforeunload', beforeUnloadHandler)
|
||||
}
|
||||
|
||||
function removeUnloadMessage() {
|
||||
if (beforeUnloadHandler) {
|
||||
window.removeEventListener('beforeunload', beforeUnloadHandler)
|
||||
beforeUnloadHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
function findDomForWindow(sourceWindow) {
|
||||
const iframes = document.getElementsByTagName('IFRAME')
|
||||
for (let i = 0; i < iframes.length; i += 1) {
|
||||
if (iframes[i].contentWindow === sourceWindow) {
|
||||
return iframes[i]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export {setUnloadMessage, removeUnloadMessage, findDomForWindow}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
let beforeUnloadHandler: null | ((e: BeforeUnloadEvent) => void) = null
|
||||
|
||||
export function setUnloadMessage(msg: string) {
|
||||
removeUnloadMessage()
|
||||
|
||||
beforeUnloadHandler = function (e: BeforeUnloadEvent) {
|
||||
return (e.returnValue = msg || '')
|
||||
}
|
||||
window.addEventListener('beforeunload', beforeUnloadHandler)
|
||||
}
|
||||
|
||||
export function removeUnloadMessage() {
|
||||
if (beforeUnloadHandler) {
|
||||
window.removeEventListener('beforeunload', beforeUnloadHandler)
|
||||
beforeUnloadHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
// disabling b/c eslint fails, saying 'MessageEventSource' is not defined, but it's
|
||||
// defined in lib.dom.d.ts
|
||||
// eslint-disable-next-line no-undef
|
||||
export function findDomForWindow(sourceWindow?: MessageEventSource | null) {
|
||||
const iframes = Array.from(document.getElementsByTagName('iframe'))
|
||||
const iframe = iframes.find(iframe => iframe.contentWindow === sourceWindow)
|
||||
return iframe || null
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/70029241
|
||||
// this can be removed and replace with `k in o`
|
||||
// when we upgrade to TS 4.9+
|
||||
export function hasKey<K extends string, T extends object>(
|
||||
k: K,
|
||||
o: T
|
||||
): o is T & Record<K, unknown> {
|
||||
return k in o
|
||||
}
|
||||
|
||||
export function getKey(key: string, o: unknown): unknown {
|
||||
return typeof o === 'object' && o !== null && hasKey(key, o) ? o[key] : undefined
|
||||
}
|
Loading…
Reference in New Issue