2016-03-24 20:15:04 +00:00
|
|
|
'use strict'
|
2016-03-18 18:51:02 +00:00
|
|
|
|
2018-10-06 11:48:00 +00:00
|
|
|
const { BrowserWindow, webContents } = require('electron')
|
2019-03-18 19:37:06 +00:00
|
|
|
const { isSameOrigin } = process.electronBinding('v8_util')
|
2019-02-04 22:49:53 +00:00
|
|
|
const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal')
|
2019-07-08 23:43:49 +00:00
|
|
|
const ipcMainUtils = require('@electron/internal/browser/ipc-main-internal-utils')
|
2018-09-20 03:43:26 +00:00
|
|
|
const parseFeaturesString = require('@electron/internal/common/parse-features-string')
|
2016-01-12 02:40:23 +00:00
|
|
|
|
2016-06-09 17:35:48 +00:00
|
|
|
const hasProp = {}.hasOwnProperty
|
2017-04-25 20:53:29 +00:00
|
|
|
const frameToGuest = new Map()
|
2016-01-12 02:40:23 +00:00
|
|
|
|
2017-05-17 20:37:23 +00:00
|
|
|
// Security options that child windows will always inherit from parent windows
|
|
|
|
const inheritedWebPreferences = new Map([
|
|
|
|
['contextIsolation', true],
|
|
|
|
['javascript', false],
|
2017-07-10 22:44:40 +00:00
|
|
|
['nativeWindowOpen', true],
|
2017-05-17 20:37:23 +00:00
|
|
|
['nodeIntegration', false],
|
2018-10-13 17:50:07 +00:00
|
|
|
['enableRemoteModule', false],
|
2017-07-10 23:23:04 +00:00
|
|
|
['sandbox', true],
|
2019-01-22 19:24:46 +00:00
|
|
|
['webviewTag', false],
|
|
|
|
['nodeIntegrationInSubFrames', false]
|
2017-05-17 20:37:23 +00:00
|
|
|
])
|
|
|
|
|
2016-01-14 18:35:29 +00:00
|
|
|
// Copy attribute of |parent| to |child| if it is not defined in |child|.
|
2017-01-04 22:50:05 +00:00
|
|
|
const mergeOptions = function (child, parent, visited) {
|
|
|
|
// Check for circular reference.
|
|
|
|
if (visited == null) visited = new Set()
|
|
|
|
if (visited.has(parent)) return
|
|
|
|
|
|
|
|
visited.add(parent)
|
|
|
|
for (const key in parent) {
|
2019-05-20 10:55:46 +00:00
|
|
|
if (key === 'type') continue
|
2016-03-24 20:15:04 +00:00
|
|
|
if (!hasProp.call(parent, key)) continue
|
2018-08-22 16:16:46 +00:00
|
|
|
if (key in child && key !== 'webPreferences') continue
|
2017-01-04 22:50:05 +00:00
|
|
|
|
|
|
|
const value = parent[key]
|
2019-08-05 19:50:51 +00:00
|
|
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
2018-08-22 16:16:46 +00:00
|
|
|
child[key] = mergeOptions(child[key] || {}, value, visited)
|
2017-01-04 22:50:05 +00:00
|
|
|
} else {
|
|
|
|
child[key] = value
|
2016-01-12 02:40:23 +00:00
|
|
|
}
|
|
|
|
}
|
2017-01-04 22:50:05 +00:00
|
|
|
visited.delete(parent)
|
|
|
|
|
2016-03-24 20:15:04 +00:00
|
|
|
return child
|
|
|
|
}
|
2016-01-12 02:40:23 +00:00
|
|
|
|
2016-01-14 18:35:29 +00:00
|
|
|
// Merge |options| with the |embedder|'s window's options.
|
2016-06-09 17:35:48 +00:00
|
|
|
const mergeBrowserWindowOptions = function (embedder, options) {
|
2016-10-07 00:45:13 +00:00
|
|
|
if (options.webPreferences == null) {
|
|
|
|
options.webPreferences = {}
|
|
|
|
}
|
2016-01-12 02:40:23 +00:00
|
|
|
if (embedder.browserWindowOptions != null) {
|
2018-03-13 07:18:50 +00:00
|
|
|
let parentOptions = embedder.browserWindowOptions
|
2018-03-12 15:28:34 +00:00
|
|
|
|
|
|
|
// if parent's visibility is available, that overrides 'show' flag (#12125)
|
|
|
|
const win = BrowserWindow.fromWebContents(embedder.webContents)
|
|
|
|
if (win != null) {
|
2018-09-13 16:10:51 +00:00
|
|
|
parentOptions = { ...embedder.browserWindowOptions, show: win.isVisible() }
|
2018-03-12 15:28:34 +00:00
|
|
|
}
|
|
|
|
|
2016-01-14 18:35:29 +00:00
|
|
|
// Inherit the original options if it is a BrowserWindow.
|
2018-03-12 15:28:34 +00:00
|
|
|
mergeOptions(options, parentOptions)
|
2016-01-12 02:40:23 +00:00
|
|
|
} else {
|
2017-05-24 18:30:59 +00:00
|
|
|
// Or only inherit webPreferences if it is a webview.
|
2018-03-15 04:56:46 +00:00
|
|
|
mergeOptions(options.webPreferences, embedder.getLastWebPreferences())
|
2016-01-12 02:40:23 +00:00
|
|
|
}
|
2016-03-30 17:51:56 +00:00
|
|
|
|
2017-05-17 20:37:23 +00:00
|
|
|
// Inherit certain option values from parent window
|
2018-08-09 17:15:23 +00:00
|
|
|
const webPreferences = embedder.getLastWebPreferences()
|
2017-05-17 20:37:23 +00:00
|
|
|
for (const [name, value] of inheritedWebPreferences) {
|
2018-08-09 17:15:23 +00:00
|
|
|
if (webPreferences[name] === value) {
|
2017-05-17 20:37:23 +00:00
|
|
|
options.webPreferences[name] = value
|
|
|
|
}
|
2017-04-21 17:59:33 +00:00
|
|
|
}
|
|
|
|
|
2016-09-29 13:43:40 +00:00
|
|
|
// Sets correct openerId here to give correct options to 'new-window' event handler
|
|
|
|
options.webPreferences.openerId = embedder.id
|
|
|
|
|
2016-03-24 20:15:04 +00:00
|
|
|
return options
|
|
|
|
}
|
2016-01-12 02:40:23 +00:00
|
|
|
|
2016-09-16 11:33:02 +00:00
|
|
|
// Setup a new guest with |embedder|
|
2016-09-29 14:01:05 +00:00
|
|
|
const setupGuest = function (embedder, frameName, guest, options) {
|
2016-01-14 18:44:21 +00:00
|
|
|
// When |embedder| is destroyed we should also destroy attached guest, and if
|
|
|
|
// guest is closed by user then we should prevent |embedder| from double
|
|
|
|
// closing guest.
|
2016-10-05 09:46:55 +00:00
|
|
|
const guestId = guest.webContents.id
|
2016-06-09 17:35:48 +00:00
|
|
|
const closedByEmbedder = function () {
|
2016-03-24 20:15:04 +00:00
|
|
|
guest.removeListener('closed', closedByUser)
|
2016-06-09 17:35:48 +00:00
|
|
|
guest.destroy()
|
2016-03-24 20:15:04 +00:00
|
|
|
}
|
2016-06-09 17:35:48 +00:00
|
|
|
const closedByUser = function () {
|
2018-10-06 11:48:00 +00:00
|
|
|
embedder._sendInternal('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_' + guestId)
|
2018-12-05 08:03:39 +00:00
|
|
|
embedder.removeListener('current-render-view-deleted', closedByEmbedder)
|
2016-03-24 20:15:04 +00:00
|
|
|
}
|
2018-12-05 08:03:39 +00:00
|
|
|
embedder.once('current-render-view-deleted', closedByEmbedder)
|
2017-07-11 00:50:54 +00:00
|
|
|
guest.once('closed', closedByUser)
|
2016-01-12 02:40:23 +00:00
|
|
|
if (frameName) {
|
2017-04-25 20:53:29 +00:00
|
|
|
frameToGuest.set(frameName, guest)
|
2016-03-24 20:15:04 +00:00
|
|
|
guest.frameName = frameName
|
|
|
|
guest.once('closed', function () {
|
2017-04-25 20:53:29 +00:00
|
|
|
frameToGuest.delete(frameName)
|
2016-03-24 20:15:04 +00:00
|
|
|
})
|
2016-01-12 02:40:23 +00:00
|
|
|
}
|
2016-10-05 09:46:55 +00:00
|
|
|
return guestId
|
2016-09-29 14:01:05 +00:00
|
|
|
}
|
2016-09-16 11:33:02 +00:00
|
|
|
|
|
|
|
// Create a new guest created by |embedder| with |options|.
|
2018-04-05 23:13:24 +00:00
|
|
|
const createGuest = function (embedder, url, referrer, frameName, options, postData) {
|
2017-04-25 20:53:29 +00:00
|
|
|
let guest = frameToGuest.get(frameName)
|
2016-09-16 11:33:02 +00:00
|
|
|
if (frameName && (guest != null)) {
|
|
|
|
guest.loadURL(url)
|
2016-10-05 09:46:55 +00:00
|
|
|
return guest.webContents.id
|
2016-09-16 11:33:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Remember the embedder window's id.
|
|
|
|
if (options.webPreferences == null) {
|
|
|
|
options.webPreferences = {}
|
|
|
|
}
|
2016-09-29 13:43:40 +00:00
|
|
|
|
2016-09-16 11:33:02 +00:00
|
|
|
guest = new BrowserWindow(options)
|
2018-12-05 08:03:39 +00:00
|
|
|
if (!options.webContents) {
|
2016-09-29 13:37:28 +00:00
|
|
|
// We should not call `loadURL` if the window was constructed from an
|
2018-12-05 08:03:39 +00:00
|
|
|
// existing webContents (window.open in a sandboxed renderer).
|
2016-09-29 13:37:28 +00:00
|
|
|
//
|
|
|
|
// Navigating to the url when creating the window from an existing
|
2018-12-05 08:03:39 +00:00
|
|
|
// webContents is not necessary (it will navigate there anyway).
|
2018-04-05 23:13:24 +00:00
|
|
|
const loadOptions = {
|
|
|
|
httpReferrer: referrer
|
|
|
|
}
|
2016-11-11 17:22:45 +00:00
|
|
|
if (postData != null) {
|
2016-10-09 23:30:38 +00:00
|
|
|
loadOptions.postData = postData
|
2016-11-10 13:55:34 +00:00
|
|
|
loadOptions.extraHeaders = 'content-type: application/x-www-form-urlencoded'
|
2016-11-11 17:22:45 +00:00
|
|
|
if (postData.length > 0) {
|
2016-11-10 13:55:34 +00:00
|
|
|
const postDataFront = postData[0].bytes.toString()
|
2016-11-11 17:22:45 +00:00
|
|
|
const boundary = /^--.*[^-\r\n]/.exec(postDataFront)
|
|
|
|
if (boundary != null) {
|
2016-11-10 13:55:34 +00:00
|
|
|
loadOptions.extraHeaders = `content-type: multipart/form-data; boundary=${boundary[0].substr(2)}`
|
|
|
|
}
|
|
|
|
}
|
2016-10-10 12:58:56 +00:00
|
|
|
}
|
2016-10-09 23:30:38 +00:00
|
|
|
guest.loadURL(url, loadOptions)
|
2016-09-29 13:37:28 +00:00
|
|
|
}
|
2016-06-09 17:35:48 +00:00
|
|
|
|
2016-09-29 14:01:05 +00:00
|
|
|
return setupGuest(embedder, frameName, guest, options)
|
2016-06-09 18:29:38 +00:00
|
|
|
}
|
|
|
|
|
2016-11-23 18:23:47 +00:00
|
|
|
const getGuestWindow = function (guestContents) {
|
2016-06-09 18:29:38 +00:00
|
|
|
let guestWindow = BrowserWindow.fromWebContents(guestContents)
|
2016-06-09 20:53:36 +00:00
|
|
|
if (guestWindow == null) {
|
|
|
|
const hostContents = guestContents.hostWebContents
|
|
|
|
if (hostContents != null) {
|
|
|
|
guestWindow = BrowserWindow.fromWebContents(hostContents)
|
|
|
|
}
|
2016-06-09 18:29:38 +00:00
|
|
|
}
|
2019-07-08 23:43:49 +00:00
|
|
|
if (!guestWindow) {
|
|
|
|
throw new Error('getGuestWindow failed')
|
|
|
|
}
|
2016-06-09 18:29:38 +00:00
|
|
|
return guestWindow
|
2016-03-24 20:15:04 +00:00
|
|
|
}
|
2016-01-12 02:40:23 +00:00
|
|
|
|
2019-07-04 16:22:08 +00:00
|
|
|
const isChildWindow = function (sender, target) {
|
|
|
|
return target.getLastWebPreferences().openerId === sender.id
|
|
|
|
}
|
|
|
|
|
|
|
|
const isRelatedWindow = function (sender, target) {
|
|
|
|
return isChildWindow(sender, target) || isChildWindow(target, sender)
|
|
|
|
}
|
|
|
|
|
|
|
|
const isScriptableWindow = function (sender, target) {
|
|
|
|
return isRelatedWindow(sender, target) && isSameOrigin(sender.getURL(), target.getURL())
|
|
|
|
}
|
|
|
|
|
|
|
|
const isNodeIntegrationEnabled = function (sender) {
|
|
|
|
return sender.getLastWebPreferences().nodeIntegration === true
|
|
|
|
}
|
|
|
|
|
2016-11-15 08:45:34 +00:00
|
|
|
// Checks whether |sender| can access the |target|:
|
|
|
|
const canAccessWindow = function (sender, target) {
|
2019-07-04 16:22:08 +00:00
|
|
|
return isChildWindow(sender, target) ||
|
|
|
|
isScriptableWindow(sender, target) ||
|
|
|
|
isNodeIntegrationEnabled(sender)
|
2016-11-15 08:45:34 +00:00
|
|
|
}
|
|
|
|
|
2017-01-12 00:36:59 +00:00
|
|
|
// Routed window.open messages with raw options
|
2019-02-04 22:49:53 +00:00
|
|
|
ipcMainInternal.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', (event, url, frameName, features) => {
|
2017-01-12 00:36:59 +00:00
|
|
|
if (url == null || url === '') url = 'about:blank'
|
|
|
|
if (frameName == null) frameName = ''
|
|
|
|
if (features == null) features = ''
|
|
|
|
|
|
|
|
const options = {}
|
|
|
|
|
|
|
|
const ints = ['x', 'y', 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'zoomFactor']
|
2018-10-13 17:50:07 +00:00
|
|
|
const webPreferences = ['zoomFactor', 'nodeIntegration', 'enableRemoteModule', 'preload', 'javascript', 'contextIsolation', 'webviewTag']
|
2017-01-12 00:36:59 +00:00
|
|
|
const disposition = 'new-window'
|
|
|
|
|
|
|
|
// Used to store additional features
|
|
|
|
const additionalFeatures = []
|
|
|
|
|
|
|
|
// Parse the features
|
|
|
|
parseFeaturesString(features, function (key, value) {
|
|
|
|
if (value === undefined) {
|
|
|
|
additionalFeatures.push(key)
|
|
|
|
} else {
|
2017-04-25 21:20:39 +00:00
|
|
|
// Don't allow webPreferences to be set since it must be an object
|
|
|
|
// that cannot be directly overridden
|
|
|
|
if (key === 'webPreferences') return
|
|
|
|
|
2017-01-12 00:36:59 +00:00
|
|
|
if (webPreferences.includes(key)) {
|
|
|
|
if (options.webPreferences == null) {
|
|
|
|
options.webPreferences = {}
|
|
|
|
}
|
|
|
|
options.webPreferences[key] = value
|
|
|
|
} else {
|
|
|
|
options[key] = value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
if (options.left) {
|
|
|
|
if (options.x == null) {
|
|
|
|
options.x = options.left
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (options.top) {
|
|
|
|
if (options.y == null) {
|
|
|
|
options.y = options.top
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (options.title == null) {
|
|
|
|
options.title = frameName
|
|
|
|
}
|
|
|
|
if (options.width == null) {
|
|
|
|
options.width = 800
|
|
|
|
}
|
|
|
|
if (options.height == null) {
|
|
|
|
options.height = 600
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const name of ints) {
|
|
|
|
if (options[name] != null) {
|
|
|
|
options[name] = parseInt(options[name], 10)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-05 23:13:24 +00:00
|
|
|
const referrer = { url: '', policy: 'default' }
|
2019-02-04 22:49:53 +00:00
|
|
|
ipcMainInternal.emit('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', event,
|
2018-09-13 16:10:51 +00:00
|
|
|
url, referrer, frameName, disposition, options, additionalFeatures)
|
2017-01-12 00:36:59 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// Routed window.open messages with fully parsed options
|
2019-02-04 22:49:53 +00:00
|
|
|
ipcMainInternal.on('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', function (event, url, referrer,
|
2018-09-13 16:10:51 +00:00
|
|
|
frameName, disposition, options,
|
|
|
|
additionalFeatures, postData) {
|
2016-03-24 20:15:04 +00:00
|
|
|
options = mergeBrowserWindowOptions(event.sender, options)
|
2018-04-05 23:13:24 +00:00
|
|
|
event.sender.emit('new-window', event, url, frameName, disposition, options, additionalFeatures, referrer)
|
2018-09-13 16:10:51 +00:00
|
|
|
const { newGuest } = event
|
2019-05-27 00:44:54 +00:00
|
|
|
if ((event.sender.getType() === 'webview' && event.sender.getLastWebPreferences().disablePopups) || event.defaultPrevented) {
|
2017-05-23 21:49:00 +00:00
|
|
|
if (newGuest != null) {
|
2017-03-28 10:58:23 +00:00
|
|
|
if (options.webContents === newGuest.webContents) {
|
2017-03-28 10:58:58 +00:00
|
|
|
// the webContents is not changed, so set defaultPrevented to false to
|
|
|
|
// stop the callers of this event from destroying the webContents.
|
2017-03-23 13:43:06 +00:00
|
|
|
event.defaultPrevented = false
|
|
|
|
}
|
2016-09-29 14:01:05 +00:00
|
|
|
event.returnValue = setupGuest(event.sender, frameName, newGuest, options)
|
2016-09-29 12:41:35 +00:00
|
|
|
} else {
|
2016-09-16 11:33:02 +00:00
|
|
|
event.returnValue = null
|
2016-09-29 12:41:35 +00:00
|
|
|
}
|
2016-01-12 02:40:23 +00:00
|
|
|
} else {
|
2018-04-05 23:13:24 +00:00
|
|
|
event.returnValue = createGuest(event.sender, url, referrer, frameName, options, postData)
|
2016-01-12 02:40:23 +00:00
|
|
|
}
|
2016-03-24 20:15:04 +00:00
|
|
|
})
|
2016-01-12 02:40:23 +00:00
|
|
|
|
2019-07-08 23:43:49 +00:00
|
|
|
const handleMessage = function (channel, handler) {
|
|
|
|
ipcMainUtils.handle(channel, (event, guestId, ...args) => {
|
|
|
|
const guestContents = webContents.fromId(guestId)
|
|
|
|
if (!guestContents) {
|
|
|
|
throw new Error(`Invalid guestId: ${guestId}`)
|
|
|
|
}
|
2016-11-23 18:23:47 +00:00
|
|
|
|
2019-07-08 23:43:49 +00:00
|
|
|
return handler(event, guestContents, ...args)
|
|
|
|
})
|
|
|
|
}
|
2016-01-12 02:40:23 +00:00
|
|
|
|
2018-12-04 15:12:21 +00:00
|
|
|
const windowMethods = new Set([
|
2019-07-08 23:43:49 +00:00
|
|
|
'destroy',
|
2018-12-04 15:12:21 +00:00
|
|
|
'focus',
|
|
|
|
'blur'
|
|
|
|
])
|
|
|
|
|
2019-07-08 23:43:49 +00:00
|
|
|
handleMessage('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', (event, guestContents, method, ...args) => {
|
|
|
|
if (!canAccessWindow(event.sender, guestContents)) {
|
|
|
|
console.error(`Blocked ${event.sender.getURL()} from accessing guestId: ${guestContents.id}`)
|
|
|
|
throw new Error(`Access denied to guestId: ${guestContents.id}`)
|
2016-11-23 18:23:47 +00:00
|
|
|
}
|
|
|
|
|
2019-07-08 23:43:49 +00:00
|
|
|
if (!windowMethods.has(method)) {
|
|
|
|
console.error(`Blocked ${event.sender.getURL()} from calling method: ${method}`)
|
|
|
|
throw new Error(`Invalid method: ${method}`)
|
2016-11-16 01:41:15 +00:00
|
|
|
}
|
|
|
|
|
2019-07-08 23:43:49 +00:00
|
|
|
return getGuestWindow(guestContents)[method](...args)
|
2016-03-24 20:15:04 +00:00
|
|
|
})
|
2016-01-12 02:40:23 +00:00
|
|
|
|
2019-07-08 23:43:49 +00:00
|
|
|
handleMessage('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', (event, guestContents, message, targetOrigin, sourceOrigin) => {
|
2017-01-12 00:36:59 +00:00
|
|
|
if (targetOrigin == null) {
|
|
|
|
targetOrigin = '*'
|
|
|
|
}
|
|
|
|
|
2016-11-15 08:45:34 +00:00
|
|
|
// The W3C does not seem to have word on how postMessage should work when the
|
|
|
|
// origins do not match, so we do not do |canAccessWindow| check here since
|
2016-11-16 01:41:15 +00:00
|
|
|
// postMessage across origins is useful and not harmful.
|
2017-04-26 17:53:54 +00:00
|
|
|
if (targetOrigin === '*' || isSameOrigin(guestContents.getURL(), targetOrigin)) {
|
2016-06-09 18:29:38 +00:00
|
|
|
const sourceId = event.sender.id
|
2018-10-06 11:48:00 +00:00
|
|
|
guestContents._sendInternal('ELECTRON_GUEST_WINDOW_POSTMESSAGE', sourceId, message, sourceOrigin)
|
2016-01-12 02:40:23 +00:00
|
|
|
}
|
2016-03-24 20:15:04 +00:00
|
|
|
})
|
2016-01-12 02:40:23 +00:00
|
|
|
|
2018-12-04 15:12:21 +00:00
|
|
|
const webContentsMethods = new Set([
|
|
|
|
'getURL',
|
2019-07-08 23:43:49 +00:00
|
|
|
'loadURL',
|
|
|
|
'executeJavaScript',
|
|
|
|
'print'
|
2018-12-04 15:12:21 +00:00
|
|
|
])
|
|
|
|
|
2019-07-08 23:43:49 +00:00
|
|
|
handleMessage('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', (event, guestContents, method, ...args) => {
|
|
|
|
if (!canAccessWindow(event.sender, guestContents)) {
|
|
|
|
console.error(`Blocked ${event.sender.getURL()} from accessing guestId: ${guestContents.id}`)
|
|
|
|
throw new Error(`Access denied to guestId: ${guestContents.id}`)
|
2016-11-25 18:03:47 +00:00
|
|
|
}
|
|
|
|
|
2019-07-08 23:43:49 +00:00
|
|
|
if (!webContentsMethods.has(method)) {
|
|
|
|
console.error(`Blocked ${event.sender.getURL()} from calling method: ${method}`)
|
|
|
|
throw new Error(`Invalid method: ${method}`)
|
2016-11-25 18:03:47 +00:00
|
|
|
}
|
2019-07-08 23:43:49 +00:00
|
|
|
|
|
|
|
return guestContents[method](...args)
|
2016-11-25 18:03:47 +00:00
|
|
|
})
|