Skip to main content

无沙箱扩展

Unsandboxed extensions run as plain <script> tags in the main window rather than in a sandbox. They have access to a lot of new powers and responsibilities that we will discuss below.

URL 限制

To protect users from malicious extensions, extensions loaded from URLs will only run unsandboxed if their URL begins with one of these exactly:

  • https://extensions.turbowarp.org/
  • http://localhost:8000/

As you don't have control over extensions.turbowarp.org, you will have to use the latter option. For this, configure your local HTTP server to run on port 8000 instead of what you've been using so far.

When manually loading an extension from a file or JavaScript source code, there is an option to load the extension without the sandbox. This option to force an extension to run unsandboxed does not exist when using URLs due to security concerns.

语法结构

The syntax for unsandboxed extensions is very familiar but has some differences. Technically, if you just copy and paste your old sandboxed extensions as unsandboxed extensions, it will appear to just work. However, this is dangerous and is likely to cause bugs later.

If your sandboxed extension has code like like this:

// Old sandboxed extensions (worker or <iframe> sandbox):
class MyExtension {
getInfo () {
return { /* ... */ };
}
}
Scratch.extensions.register(new MyExtension());

Or if your extension uses an old "plugin" mechanism, such as this one: (if you don't recognize this code then don't worry about it)

class MyExtension {
getInfo () {
return { /* ... */ };
}
}
(function() {
var extensionInstance = new MyExtension(window.vm.extensionManager.runtime)
var serviceName = window.vm.extensionManager._registerInternalExtension(extensionInstance)
window.vm.extensionManager._loadedExtensions.set(extensionInstance.getInfo().id, serviceName)
})();

The unsandboxed version would have code like this:

(function(Scratch) {
'use strict';
class MyExtension {
getInfo () {
return { /* ... */ };
}
}
Scratch.extensions.register(new MyExtension());
})(Scratch);

Using this template prevents unsandboxed extensions from interfering with each other when they try to define variables, classes, or functions with the same name. By requiring everything to be defined in an immediately-invoked-function-expression (IIFE) and enabling strict mode, we prevent variables from accidentally leaking to the global scope.

All functions and variables defined by the extension must be defined within the IIFE. Additionally, each extension must make sure to use its own personal copy of the Scratch API, which this template does automatically.

An interesting thing to note about this template is that it is backward compatible with sandboxed extensions. As long as the extension doesn't use any of the features given to unsandboxed extensions, it will continue to work the same as a sandboxed extension.

一个更完善的示例

Here you can see a complete unsandboxed extension:

unsandboxed/hello-world-unsandboxed.js - 在 TurboWarp 中尝试这个扩展
(function(Scratch) {
'use strict';

if (!Scratch.extensions.unsandboxed) {
throw new Error('这个“你好,世界”扩展必须不使用沙箱运行');
}

class HelloWorld {
getInfo() {
return {
id: 'helloworldunsandboxed',
name: '无沙箱的你好世界',
blocks: [
{
opcode: 'hello',
blockType: Scratch.BlockType.REPORTER,
text: '你好!'
}
]
};
}
hello() {
return '世界';
}
}
Scratch.extensions.register(new HelloWorld());
})(Scratch);

If you're using a local HTTP server, save this so you can access it through the server, then load the exact URL http://localhost:8000/hello-world-unsandboxed.js in TurboWarp. If nothing appears, see the developer console. If you see an error that the extension must be run unsandboxed, most likely you are using an old version of TurboWarp or you didn't load it from a URL that starts with http://localhost:8000/ exactly. 127.0.0.1 and 0.0.0.0 won't work! It must be localhost, port 8000 exactly.

If you're just using files, make sure to check the "Run extension without sandbox" box each time you load the extension.

Create a new empty project with a repeat (30) loop that adds the "hello" block to a list. Notice that it now runs instantly while the sandboxed version would've taken at least a second.

Observe that the majority of the code is still identical: You still create a class, then call Scratch.extensions.register(), then Scratch calls getInfo() which returns the same type of object. Just the surrounding template is different.

能力越大,责任越大

Before we talk about the new APIs, we want to note some additional requirements for unsandboxed extensions:

  • Blocks must not throw errors. While sandboxed extensions could, unsandboxed extensions that do this may break scripts.
  • Input and boolean blocks must return a valid value. While sandboxed extensions are free to neglect this, unsandboxed extensions that don't return proper values (string, number, or boolean) can break scripts in unknown ways.
  • Blocks must not get stuck in infinite loops. While sandboxed extensions will usually not be able to freeze the entire window if they get stuck in a loop, unsandboxed extensions will. This can result in data loss.

访问 Scratch 内部接口

The big thing that unsandboxed extensions can do is directly access Scratch internals.

  const vm = Scratch.vm;

That's full access to the actual Scratch VM object. There is a lot you can do with this.

Remember -- every variable declaration must happen inside the IIFE.

// GOOD CODE
(function(Scratch) {
const vm = Scratch.vm;
// ...
}(Scratch));

// BAD CODE
const vm = Scratch.vm;
(function(Scratch) {
// ...
}(Scratch));

Dig around for a while to find what you're looking for. Your developer tools will be immensely useful as you can access Scratch from there after an extension is loaded, or use the other debugging global variables that are available (but please don't use those in extensions). You may find the scratch-vm source code or @turbowarp/types to be useful resources.

Here is an example of an extension that uses Scratch.vm to toggle turbo mode, similar to the "runtime options" extension on extensions.turbowarp.org:

unsandboxed/turbo-mode.js - 在 TurboWarp 中尝试这个扩展
(function(Scratch) {
'use strict';

if (!Scratch.extensions.unsandboxed) {
throw new Error('这个加速模式示例必须不使用沙箱运行');
}
const vm = Scratch.vm;

class TurboMode {
getInfo() {
return {
id: 'turbomodeunsandboxed',
name: '加速模式',
blocks: [
{
opcode: 'set',
blockType: Scratch.BlockType.COMMAND,
text: '[ENABLED] 加速模式',
arguments: {
ENABLED: {
type: Scratch.ArgumentType.STRING,
menu: 'ENABLED_MENU'
}
}
}
],
menus: {
ENABLED_MENU: {
acceptReporters: true,
items: ['启用', '禁用']
}
}
};
}
set(args) {
vm.setTurboMode(args.ENABLED === '启用');
}
}
Scratch.extensions.register(new TurboMode());
})(Scratch);

“积木工具” BlockUtility 对象

When a sandboxed custom extension is run, all it receives are the arguments that the scripts provided. It doesn't even know which sprite is executing it. We now introduce the second argument passed to block functions: BlockUtility.

The BlockUtility object, conventionally called util, allows blocks in unsandboxed extensions to get direct access to the sprite that is running them using util.target. Similar to the VM, this is the actual object used internally. You have full access to it.

Here is an example extension that demonstrates using util.target to get the name of the current sprite or access variables.

unsandboxed/block-utility-examples.js - 在 TurboWarp 中尝试这个扩展
(function(Scratch) {
'use strict';

if (!Scratch.extensions.unsandboxed) {
throw new Error('这个“积木工具”示例必须不使用沙箱运行');
}

class BlockUtilityExamples {
getInfo() {
return {
id: 'blockutilityexamples',
name: '积木工具 (BlockUtility) 示例',
blocks: [
{
opcode: 'getSpriteName',
text: '角色的名字',
blockType: Scratch.BlockType.REPORTER,
},
{
opcode: 'doesVariableExist',
text: '有一个类型为 [TYPE] 的 [NAME] 变量么?',
blockType: Scratch.BlockType.BOOLEAN,
arguments: {
NAME: {
type: Scratch.ArgumentType.STRING,
defaultValue: '我的变量'
},
TYPE: {
type: Scratch.ArgumentType.STRING,
menu: 'TYPE_MENU',
defaultValue: 'list'
}
}
}
],
menus: {
TYPE_MENU: {
acceptReporters: true,
items: [
// value 代表的是 scratch-vm 内部变量的类型。
// 并且是的,广播消息也是变量。
// https://github.com/TurboWarp/scratch-vm/blob/20c60193c1c567a65cca87b16d22c51963565a43/src/engine/variable.js#L43-L67
{
text: '变量',
value: ''
},
{
text: '列表',
value: 'list'
},
{
text: '广播消息',
value: 'broadcast_msg'
}
]
}
}
};
}
getSpriteName(args, util) {
return util.target.getName();
}
doesVariableExist(args, util) {
const variable = util.target.lookupVariableByNameAndType(args.NAME.toString(), args.TYPE);
// 记住:布尔值积木必须返回一个 boolean 类型的值,它们需要自己完成类型转换。
return !!variable;
}
}
Scratch.extensions.register(new BlockUtilityExamples());
})(Scratch);

Note that every sprite, script, and block shares the same block utility object. Instead of making a object each time your block runs, it just updates the properties of the shared object for performance. Thus, the only safe time to access util is immediately when the block runs. Trying to access util in a setTimeout, setInterval, Promise callback, or other non-syncronous callback will not work correctly. If you need to access properties from util later, save them in a variable ahead of time.

  // This is NOT reliable and may alert the wrong thing:
myBlock(args, util) {
setTimeout(() => {
alert(util.target.getName());
}, 1000);
}

// This will always work:
myBlock(args, util) {
const target = util.target;
setTimeout(() => {
alert(target.getName());
}, 1000);
}

通用模板

Here are some common copy-and-pasteable code snippets that can be used:

If the extension MUST be run unsandboxed, add this around the start:

  if (!Scratch.extensions.unsandboxed) {
throw new Error('Extension Name must run unsandboxed');
}

If you're using the vm, runtime or Cast APIs a lot, common practise is to define them around the start to save time:

  const vm = Scratch.vm;
const runtime = vm.runtime;
const Cast = Scratch.Cast; // Discussed later.

带权限的 API

Whereas sandboxed extensions are free to use APIs such as fetch() as they please, unsandboxed extensions should instead ask for permission before making a request to any remote service. This gives the user control over their privacy. While there is no technical measures enforcing this at runtime, it is required for all extensions on extensions.turbowarp.org.

Requests to some popular services such as GitHub Pages or GitLab Pages may be automatically approved, while requests to other random websites may show a prompt to the user. You shouldn't make any assumptions about this, and your code needs to ensure that it can gracefully handle the user rejecting the prompt (the extension should behave the same as it does when there is no internet connection).

These permissioned APIs will also automatically prevent projects from running arbitrary JavaScript by attempting to, for example, redirect to a javascript: URL.

网络请求 API、WebSocket、图像、音频文件等等

Use Scratch.fetch(url) instead of fetch(url). Check await Scratch.canFetch(url) before using other APIs that connect to remote websites.

// Do not do this:
const response = await fetch(url);
// Do this instead:
const response = await Scratch.fetch(url);

// Do not do this:
const ws = new WebSocket(url);
// Do this instead:
if (await Scratch.canFetch(url)) {
const ws = new WebSocket(url);
}

// Do not do this:
const image = new Image();
image.src = src;
// Do this instead:
if (await Scratch.canFetch(src)) {
const image = new Image();
image.src = src;
}

// Do not do this:
const audio = new Audio(url);
// Do this instead:
if (await Scratch.canFetch(url)) {
const audio = new Audio(url);
}

打开新页面或者窗口

Use Scratch.openWindow(url) instead of window.open(url). Scratch.openWindow always sets the target to "_blank" to open a new tab or window. If you can't use Scratch.openWindow(url) for some reason, check await Scratch.canOpenWindow(url) before calling window.open(url).

// Do not do this:
const win = window.open(url);
// Do this instead:
const win = await Scratch.openWindow(url);

// Do not do this:
const win = window.open(url, '_blank', 'width=400,height=400')
// Do this instead:
const win = await Scratch.openWindow(url, 'width=400,height=400');

让当前页面跳转到新网址

Use Scratch.redirect(url) instead of location.href = url. If you can't use Scratch.redirect(url), check await Scratch.canRedirect(url) before running location.href = url.

// Do not do this:
location.href = url;
// Do this instead:
await Scratch.redirect(url);

课后练习

We encourage you to try to figure these out without the hints. It will make you much more familiar with how VM internals work.

  1. Create a block that presses the green flag. (Hint: vm.greenFlag)
  2. Create a block that returns the x position of the sprite, similar to the "x position" block. (Hint: target.x)
  3. Create a block that moves the sprite to the center of the screen, similar to "go to x: 0 y: 0". (Hint: target.setXY(x, y))

下一步

Depending on the server you've been using, you might be tired of remembering to hard-reload all of the time. Let's learn about a better development server.