Spaces:
Build error
Build error
const ScratchCommon = require('./tw-extension-api-common'); | |
const createScratchX = require('./tw-scratchx-compatibility-layer'); | |
const AsyncLimiter = require('../util/async-limiter'); | |
const createTranslate = require('./tw-l10n'); | |
/** | |
* Parse a URL object or return null. | |
* @param {string} url | |
* @returns {URL|null} | |
*/ | |
const parseURL = url => { | |
try { | |
return new URL(url, location.href); | |
} catch (e) { | |
return null; | |
} | |
}; | |
/** | |
* Sets up the global.Scratch API for an unsandboxed extension. | |
* @param {VirtualMachine} vm | |
* @returns {Promise<object[]>} Resolves with a list of extension objects when Scratch.extensions.register is called. | |
*/ | |
const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => { | |
const extensionObjects = []; | |
const register = extensionObject => { | |
extensionObjects.push(extensionObject); | |
resolve(extensionObjects); | |
}; | |
// Create a new copy of global.Scratch for each extension | |
const Scratch = Object.assign({}, global.Scratch || {}, ScratchCommon); | |
Scratch.extensions = { | |
unsandboxed: true, | |
isPenguinMod: true, | |
register | |
}; | |
Scratch.vm = vm; | |
Scratch.renderer = vm.runtime.renderer; | |
Scratch.canFetch = async url => { | |
const parsed = parseURL(url); | |
if (!parsed) { | |
return false; | |
} | |
// Always allow protocols that don't involve a remote request. | |
if (parsed.protocol === 'blob:' || parsed.protocol === 'data:') { | |
return true; | |
} | |
return vm.securityManager.canFetch(parsed.href); | |
}; | |
Scratch.canOpenWindow = async url => { | |
const parsed = parseURL(url); | |
if (!parsed) { | |
return false; | |
} | |
// Always reject protocols that would allow code execution. | |
// eslint-disable-next-line no-script-url | |
if (parsed.protocol === 'javascript:') { | |
return false; | |
} | |
return vm.securityManager.canOpenWindow(parsed.href); | |
}; | |
Scratch.canRedirect = async url => { | |
const parsed = parseURL(url); | |
if (!parsed) { | |
return false; | |
} | |
// Always reject protocols that would allow code execution. | |
// eslint-disable-next-line no-script-url | |
if (parsed.protocol === 'javascript:') { | |
return false; | |
} | |
return vm.securityManager.canRedirect(parsed.href); | |
}; | |
Scratch.fetch = async (url, options) => { | |
const actualURL = url instanceof Request ? url.url : url; | |
if (!await Scratch.canFetch(actualURL)) { | |
throw new Error(`Permission to fetch ${actualURL} rejected.`); | |
} | |
return fetch(url, options); | |
}; | |
Scratch.openWindow = async (url, features) => { | |
if (!await Scratch.canOpenWindow(url)) { | |
throw new Error(`Permission to open tab ${url} rejected.`); | |
} | |
return window.open(url, '_blank', features); | |
}; | |
Scratch.redirect = async url => { | |
if (!await Scratch.canRedirect(url)) { | |
throw new Error(`Permission to redirect to ${url} rejected.`); | |
} | |
location.href = url; | |
}; | |
Scratch.canRecordAudio = async () => vm.securityManager.canRecordAudio(); | |
Scratch.canRecordVideo = async () => vm.securityManager.canRecordVideo(); | |
Scratch.canReadClipboard = async () => vm.securityManager.canReadClipboard(); | |
Scratch.canNotify = async () => vm.securityManager.canNotify(); | |
Scratch.canGeolocate = async () => vm.securityManager.canGeolocate(); | |
Scratch.canEmbed = async url => { | |
const parsed = parseURL(url); | |
if (!parsed) { | |
return false; | |
} | |
return vm.securityManager.canEmbed(parsed.href); | |
}; | |
Scratch.canUnsandbox = async () => vm.securityManager.canUnsandbox(); | |
Scratch.translate = createTranslate(vm); | |
global.Scratch = Scratch; | |
global.ScratchExtensions = createScratchX(Scratch); | |
vm.emit('CREATE_UNSANDBOXED_EXTENSION_API', Scratch); | |
}); | |
/** | |
* Disable the existing global.Scratch unsandboxed extension APIs. | |
* This helps debug poorly designed extensions. | |
*/ | |
const teardownUnsandboxedExtensionAPI = () => { | |
// We can assume global.Scratch already exists. | |
global.Scratch.extensions.register = () => { | |
throw new Error('Too late to register new extensions.'); | |
}; | |
}; | |
/** | |
* Load an unsandboxed extension from an arbitrary URL. This is dangerous. | |
* @param {string} extensionURL | |
* @param {Virtualmachine} vm | |
* @returns {Promise<object[]>} Resolves with a list of extension objects if the extension was loaded successfully. | |
*/ | |
const loadUnsandboxedExtension = (extensionURL, vm) => new Promise((resolve, reject) => { | |
setupUnsandboxedExtensionAPI(vm).then(resolve); | |
const script = document.createElement('script'); | |
script.onerror = () => { | |
reject(new Error(`Error in unsandboxed script ${extensionURL}. Check the console for more information.`)); | |
}; | |
script.src = extensionURL; | |
document.body.appendChild(script); | |
}).then(objects => { | |
teardownUnsandboxedExtensionAPI(); | |
return objects; | |
}); | |
// Because loading unsandboxed extensions requires messing with global state (global.Scratch), | |
// only let one extension load at a time. | |
const limiter = new AsyncLimiter(loadUnsandboxedExtension, 1); | |
const load = (extensionURL, vm) => limiter.do(extensionURL, vm); | |
module.exports = { | |
setupUnsandboxedExtensionAPI, | |
load | |
}; | |