Skip to content

Commit

Permalink
Merge branch 'TurboWarp:develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
FurryR authored Dec 25, 2024
2 parents 4a56912 + b900a37 commit f81e165
Show file tree
Hide file tree
Showing 12 changed files with 501 additions and 34 deletions.
10 changes: 6 additions & 4 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
with:
node-version: 20
cache: npm
Expand All @@ -29,7 +31,7 @@ jobs:
# It will still generate what it can, so it's safe to ignore the error
continue-on-error: true
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa
with:
path: ./playground/

Expand All @@ -45,4 +47,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e
6 changes: 4 additions & 2 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
persist-credentials: false
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
with:
node-version: 20
cache: npm
Expand Down
123 changes: 105 additions & 18 deletions src/engine/tw-font-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ const AssetUtil = require('../util/tw-asset-util');
const StringUtil = require('../util/string-util');
const log = require('../util/log');

/*
* In general in this file, note that font names in browsers are case-insensitive
* but are whitespace-sensitive.
*/

/**
* @typedef InternalFont
* @property {boolean} system True if the font is built in to the system
Expand All @@ -11,40 +16,121 @@ const log = require('../util/log');
* @property {Asset} [asset] scratch-storage asset if system: false
*/

/**
* @param {string} font
* @returns {string}
*/
const removeInvalidCharacters = font => font.replace(/[^-\w ]/g, '');

/**
* @param {InternalFont[]} fonts Modified in-place
* @param {InternalFont} newFont
* @returns {InternalFont|null}
*/
const addOrUpdateFont = (fonts, newFont) => {
let oldFont;
const oldIndex = fonts.findIndex(i => i.family.toLowerCase() === newFont.family.toLowerCase());
if (oldIndex !== -1) {
oldFont = fonts[oldIndex];
fonts.splice(oldIndex, 1);
}
fonts.push(newFont);
return oldFont;
};

class FontManager extends EventEmitter {
/**
* @param {Runtime} runtime
*/
constructor (runtime) {
super();

/** @type {Runtime} */
this.runtime = runtime;

/** @type {Array<InternalFont>} */
this.fonts = [];

/**
* All entries should be lowercase.
* @type {Set<string>}
*/
this.restrictedFonts = new Set();
}

/**
* @param {string} family An unknown font family
* @returns {boolean} true if the family is valid
* Prevents a family from being overridden by a custom font. The project may still use it as a system font.
* @param {string} family
*/
isValidFamily (family) {
restrictFont (family) {
if (!this.isValidSystemFont(family)) {
throw new Error('Invalid font');
}

this.restrictedFonts.add(family.toLowerCase());

const oldLength = this.fonts.length;
this.fonts = this.fonts.filter(font => font.system || this.isValidCustomFont(font.family));
if (this.fonts.length !== oldLength) {
this.updateRenderer();
this.changed();
}
}

/**
* @param {string} family Untrusted font name input
* @returns {boolean} true if the family is valid for a system font
*/
isValidSystemFont (family) {
return /^[-\w ]+$/.test(family);
}

/**
* @param {string} family
* @returns {boolean}
* @param {string} family Untrusted font name input
* @returns {boolean} true if the family is valid for a custom font
*/
hasFont (family) {
return !!this.fonts.find(i => i.family === family);
isValidCustomFont (family) {
return /^[-\w ]+$/.test(family) && !this.restrictedFonts.has(family.toLowerCase());
}

/**
* @deprecated only exists for extension compatibility, use isValidSystemFont or isValidCustomFont instead
*/
isValidFamily (family) {
return this.isValidSystemFont(family) && this.isValidCustomFont(family);
}

/**
* @param {string} family Untrusted font name input
* @returns {string}
*/
getUnusedSystemFont (family) {
return StringUtil.caseInsensitiveUnusedName(
removeInvalidCharacters(family),
this.fonts.map(i => i.family)
);
}

/**
* @param {string} family Untrusted font name input
* @returns {string}
*/
getUnusedCustomFont (family) {
return StringUtil.caseInsensitiveUnusedName(
removeInvalidCharacters(family),
[
...this.fonts.map(i => i.family),
...this.restrictedFonts
]
);
}

/**
* @param {string} family
* @returns {boolean}
*/
getSafeName (family) {
family = family.replace(/[^-\w ]/g, '');
return StringUtil.unusedName(family, this.fonts.map(i => i.family));
hasFont (family) {
return !!this.fonts.find(i => i.family.toLowerCase() === family.toLowerCase());
}

changed () {
Expand All @@ -56,14 +142,17 @@ class FontManager extends EventEmitter {
* @param {string} fallback
*/
addSystemFont (family, fallback) {
if (!this.isValidFamily(family)) {
throw new Error('Invalid family');
if (!this.isValidSystemFont(family)) {
throw new Error('Invalid system font family');
}
this.fonts.push({
const oldFont = addOrUpdateFont(this.fonts, {
system: true,
family,
fallback
});
if (oldFont && !oldFont.system) {
this.updateRenderer();
}
this.changed();
}

Expand All @@ -73,17 +162,15 @@ class FontManager extends EventEmitter {
* @param {Asset} asset scratch-storage asset
*/
addCustomFont (family, fallback, asset) {
if (!this.isValidFamily(family)) {
throw new Error('Invalid family');
if (!this.isValidCustomFont(family)) {
throw new Error('Invalid custom font family');
}

this.fonts.push({
addOrUpdateFont(this.fonts, {
system: false,
family,
fallback,
asset
});

this.updateRenderer();
this.changed();
}
Expand Down
2 changes: 2 additions & 0 deletions src/extension-support/extension-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ Object.assign(global.Scratch, ScratchCommon, {
canNotify: () => Promise.resolve(false),
canGeolocate: () => Promise.resolve(false),
canEmbed: () => Promise.resolve(false),
canDownload: () => Promise.resolve(false),
download: () => Promise.reject(new Error('Scratch.download not supported in sandboxed extensions')),
translate
});

Expand Down
10 changes: 10 additions & 0 deletions src/extension-support/tw-security-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ class SecurityManager {
canEmbed (documentURL) {
return Promise.resolve(true);
}

/**
* Determine whether an extension is allowed to download a URL with a given name.
* @param {string} resourceURL The URL to download
* @param {string} name The name of the file
* @returns {Promise<boolean>|boolean}
*/
canDownload (resourceURL, name) {
return Promise.resolve(true);
}
}

module.exports = SecurityManager;
25 changes: 25 additions & 0 deletions src/extension-support/tw-unsandboxed-extension-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => {
return vm.securityManager.canEmbed(parsed.href);
};

Scratch.canDownload = async (url, name) => {
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.canDownload(url, name);
};

Scratch.fetch = async (url, options) => {
const actualURL = url instanceof Request ? url.url : url;

Expand Down Expand Up @@ -127,6 +140,18 @@ const setupUnsandboxedExtensionAPI = vm => new Promise(resolve => {
location.href = url;
};

Scratch.download = async (url, name) => {
if (!await Scratch.canDownload(url, name)) {
throw new Error(`Permission to download ${name} rejected.`);
}
const link = document.createElement('a');
link.href = url;
link.download = name;
document.body.appendChild(link);
link.click();
link.remove();
};

Scratch.translate = createTranslate(vm);

global.Scratch = Scratch;
Expand Down
14 changes: 14 additions & 0 deletions src/util/string-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ class StringUtil {
return name + i;
}

/**
* @param {string} name
* @param {string[]} existingNames
* @returns {string}
*/
static caseInsensitiveUnusedName (name, existingNames) {
const exists = needle => existingNames.some(i => i.toLowerCase() === needle.toLowerCase());
if (!exists(name)) return name;
name = StringUtil.withoutTrailingDigits(name);
let i = 2;
while (exists(`${name}${i}`)) i++;
return `${name}${i}`;
}

/**
* Split a string on the first occurrence of a split character.
* @param {string} text - the string to split.
Expand Down
Loading

0 comments on commit f81e165

Please sign in to comment.