From 07df1e011794f5a77f7fb7da5cd9ea353a8747e2 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sun, 14 Mar 2021 18:41:15 -0400 Subject: Fix dictionary image support (#1526) * Fix content security policy for images * Add createBlobFromBase64Content to MediaUtil * Update MediaLoader to use MediaUtil * Use blob URLs when importing dictionaries * Update VM's URL to support createObjectURL and revokeObjectURL * Fix test --- dev/data/manifest-variants.json | 4 ++-- dev/database-vm.js | 15 ++++++++++++++- dev/vm.js | 22 +++++++++++++++++++--- ext/js/language/dictionary-importer.js | 4 +++- ext/js/media/media-loader.js | 17 +++++------------ ext/js/media/media-util.js | 16 ++++++++++++++++ ext/manifest.json | 2 +- ext/popup.html | 1 + ext/search.html | 1 + 9 files changed, 62 insertions(+), 20 deletions(-) diff --git a/dev/data/manifest-variants.json b/dev/data/manifest-variants.json index 14050241..14dd02e2 100644 --- a/dev/data/manifest-variants.json +++ b/dev/data/manifest-variants.json @@ -114,7 +114,7 @@ "popup.html", "template-renderer.html" ], - "content_security_policy": "default-src 'self'; style-src 'self' 'unsafe-inline'; media-src *; connect-src *" + "content_security_policy": "default-src 'self'; img-src blob: 'self'; style-src 'self' 'unsafe-inline'; media-src *; connect-src *" }, "variants": [ { @@ -194,7 +194,7 @@ { "action": "set", "path": ["content_security_policy"], - "value": "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; connect-src *" + "value": "default-src 'self'; script-src 'self' 'unsafe-eval'; img-src blob: 'self'; style-src 'self' 'unsafe-inline'; media-src *; connect-src *" }, { "action": "set", diff --git a/dev/database-vm.js b/dev/database-vm.js index e43daf2f..b682bca3 100644 --- a/dev/database-vm.js +++ b/dev/database-vm.js @@ -76,6 +76,13 @@ class Image { } } +class Blob { + constructor(array, options) { + this._array = array; + this._options = options; + } +} + async function fetch(url2) { const filePath = url.fileURLToPath(url2); await Promise.resolve(); @@ -89,15 +96,21 @@ async function fetch(url2) { }; } +function atob(data) { + return Buffer.from(data, 'base64').toString('ascii'); +} + class DatabaseVM extends VM { constructor() { super({ chrome, Image, + Blob, fetch, indexedDB: global.indexedDB, IDBKeyRange: global.IDBKeyRange, - JSZip + JSZip, + atob }); this.context.window = this.context; this.indexedDB = global.indexedDB; diff --git a/dev/vm.js b/dev/vm.js index b3fc63e7..98a1f7bf 100644 --- a/dev/vm.js +++ b/dev/vm.js @@ -19,6 +19,7 @@ const fs = require('fs'); const vm = require('vm'); const path = require('path'); const assert = require('assert'); +const crypto = require('crypto'); function getContextEnvironmentRecords(context, names) { @@ -115,9 +116,9 @@ function deepStrictEqual(actual, expected) { } -function createURLClass() { +function createURLClass(urlMap) { const BaseURL = URL; - return function URL(url) { + const result = function URL(url) { const u = new BaseURL(url); this.hash = u.hash; this.host = u.host; @@ -132,12 +133,23 @@ function createURLClass() { this.searchParams = u.searchParams; this.username = u.username; }; + result.createObjectURL = (object) => { + const id = crypto.randomBytes(16).toString('hex'); + const url = `blob:${id}`; + urlMap.set(url, object); + return url; + }; + result.revokeObjectURL = (url) => { + urlMap.delete(url); + }; + return result; } class VM { constructor(context={}) { - context.URL = createURLClass(); + this._urlMap = new Map(); + context.URL = createURLClass(this._urlMap); this._context = vm.createContext(context); this._assert = { deepStrictEqual @@ -186,6 +198,10 @@ class VM { return single ? results[0] : results; } + + getUrlObject(url) { + return this._urlMap.get(url); + } } diff --git a/ext/js/language/dictionary-importer.js b/ext/js/language/dictionary-importer.js index b4429315..888d19b0 100644 --- a/ext/js/language/dictionary-importer.js +++ b/ext/js/language/dictionary-importer.js @@ -400,7 +400,9 @@ class DictionaryImporter { eventListeners.removeAllEventListeners(); reject(new Error('Image failed to load')); }, false); - image.src = `data:${mediaType};base64,${content}`; + const blob = MediaUtil.createBlobFromBase64Content(content, mediaType); + const url = URL.createObjectURL(blob); + image.src = url; }); } } diff --git a/ext/js/media/media-loader.js b/ext/js/media/media-loader.js index d9d40a36..4ac733c5 100644 --- a/ext/js/media/media-loader.js +++ b/ext/js/media/media-loader.js @@ -15,6 +15,10 @@ * along with this program. If not, see . */ +/* global + * MediaUtil + */ + class MediaLoader { constructor() { this._token = {}; @@ -82,22 +86,11 @@ class MediaLoader { const token = this._token; const data = (await yomichan.api.getMedia([{path, dictionaryName}]))[0]; if (token === this._token && data !== null) { - const contentArrayBuffer = this._base64ToArrayBuffer(data.content); - const blob = new Blob([contentArrayBuffer], {type: data.mediaType}); + const blob = MediaUtil.createBlobFromBase64Content(data.content, data.mediaType); const url = URL.createObjectURL(blob); cachedData.data = data; cachedData.url = url; } return cachedData; } - - _base64ToArrayBuffer(content) { - const binaryContent = window.atob(content); - const length = binaryContent.length; - const array = new Uint8Array(length); - for (let i = 0; i < length; ++i) { - array[i] = binaryContent.charCodeAt(i); - } - return array.buffer; - } } diff --git a/ext/js/media/media-util.js b/ext/js/media/media-util.js index 11172c5c..f783038a 100644 --- a/ext/js/media/media-util.js +++ b/ext/js/media/media-util.js @@ -129,4 +129,20 @@ class MediaUtil { return null; } } + + /** + * Creates a new `Blob` object from a base64 string of content. + * @param content The binary content string encoded in base64. + * @param mediaType The type of the media. + * @returns A new `Blob` object corresponding to the specified content. + */ + static createBlobFromBase64Content(content, mediaType) { + const binaryContent = atob(content); + const length = binaryContent.length; + const array = new Uint8Array(length); + for (let i = 0; i < length; ++i) { + array[i] = binaryContent.charCodeAt(i); + } + return new Blob([array.buffer], {type: mediaType}); + } } diff --git a/ext/manifest.json b/ext/manifest.json index 7868924f..ee383e84 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -113,5 +113,5 @@ "popup.html", "template-renderer.html" ], - "content_security_policy": "default-src 'self'; style-src 'self' 'unsafe-inline'; media-src *; connect-src *" + "content_security_policy": "default-src 'self'; img-src blob: 'self'; style-src 'self' 'unsafe-inline'; media-src *; connect-src *" } diff --git a/ext/popup.html b/ext/popup.html index dfe547b2..78e89997 100644 --- a/ext/popup.html +++ b/ext/popup.html @@ -123,6 +123,7 @@ + diff --git a/ext/search.html b/ext/search.html index 9e4c8f2e..48abb8b7 100644 --- a/ext/search.html +++ b/ext/search.html @@ -107,6 +107,7 @@ + -- cgit v1.2.3