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:
Paul Gray 2023-01-06 10:40:51 -05:00
parent 2882b35dcf
commit 70ba806b5c
25 changed files with 491 additions and 237 deletions

View File

@ -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",

View File

@ -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
}

View File

@ -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}

View File

@ -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}

View File

@ -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)

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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
}