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
This commit is contained in:
toasted-nutbread 2021-03-14 18:41:15 -04:00 committed by GitHub
parent 52a4d874ea
commit 07df1e0117
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 62 additions and 20 deletions

View File

@ -114,7 +114,7 @@
"popup.html", "popup.html",
"template-renderer.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": [ "variants": [
{ {
@ -194,7 +194,7 @@
{ {
"action": "set", "action": "set",
"path": ["content_security_policy"], "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", "action": "set",

View File

@ -76,6 +76,13 @@ class Image {
} }
} }
class Blob {
constructor(array, options) {
this._array = array;
this._options = options;
}
}
async function fetch(url2) { async function fetch(url2) {
const filePath = url.fileURLToPath(url2); const filePath = url.fileURLToPath(url2);
await Promise.resolve(); 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 { class DatabaseVM extends VM {
constructor() { constructor() {
super({ super({
chrome, chrome,
Image, Image,
Blob,
fetch, fetch,
indexedDB: global.indexedDB, indexedDB: global.indexedDB,
IDBKeyRange: global.IDBKeyRange, IDBKeyRange: global.IDBKeyRange,
JSZip JSZip,
atob
}); });
this.context.window = this.context; this.context.window = this.context;
this.indexedDB = global.indexedDB; this.indexedDB = global.indexedDB;

View File

@ -19,6 +19,7 @@ const fs = require('fs');
const vm = require('vm'); const vm = require('vm');
const path = require('path'); const path = require('path');
const assert = require('assert'); const assert = require('assert');
const crypto = require('crypto');
function getContextEnvironmentRecords(context, names) { function getContextEnvironmentRecords(context, names) {
@ -115,9 +116,9 @@ function deepStrictEqual(actual, expected) {
} }
function createURLClass() { function createURLClass(urlMap) {
const BaseURL = URL; const BaseURL = URL;
return function URL(url) { const result = function URL(url) {
const u = new BaseURL(url); const u = new BaseURL(url);
this.hash = u.hash; this.hash = u.hash;
this.host = u.host; this.host = u.host;
@ -132,12 +133,23 @@ function createURLClass() {
this.searchParams = u.searchParams; this.searchParams = u.searchParams;
this.username = u.username; 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 { class VM {
constructor(context={}) { constructor(context={}) {
context.URL = createURLClass(); this._urlMap = new Map();
context.URL = createURLClass(this._urlMap);
this._context = vm.createContext(context); this._context = vm.createContext(context);
this._assert = { this._assert = {
deepStrictEqual deepStrictEqual
@ -186,6 +198,10 @@ class VM {
return single ? results[0] : results; return single ? results[0] : results;
} }
getUrlObject(url) {
return this._urlMap.get(url);
}
} }

View File

@ -400,7 +400,9 @@ class DictionaryImporter {
eventListeners.removeAllEventListeners(); eventListeners.removeAllEventListeners();
reject(new Error('Image failed to load')); reject(new Error('Image failed to load'));
}, false); }, false);
image.src = `data:${mediaType};base64,${content}`; const blob = MediaUtil.createBlobFromBase64Content(content, mediaType);
const url = URL.createObjectURL(blob);
image.src = url;
}); });
} }
} }

View File

@ -15,6 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
/* global
* MediaUtil
*/
class MediaLoader { class MediaLoader {
constructor() { constructor() {
this._token = {}; this._token = {};
@ -82,22 +86,11 @@ class MediaLoader {
const token = this._token; const token = this._token;
const data = (await yomichan.api.getMedia([{path, dictionaryName}]))[0]; const data = (await yomichan.api.getMedia([{path, dictionaryName}]))[0];
if (token === this._token && data !== null) { if (token === this._token && data !== null) {
const contentArrayBuffer = this._base64ToArrayBuffer(data.content); const blob = MediaUtil.createBlobFromBase64Content(data.content, data.mediaType);
const blob = new Blob([contentArrayBuffer], {type: data.mediaType});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
cachedData.data = data; cachedData.data = data;
cachedData.url = url; cachedData.url = url;
} }
return cachedData; 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;
}
} }

View File

@ -129,4 +129,20 @@ class MediaUtil {
return null; 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});
}
} }

View File

@ -113,5 +113,5 @@
"popup.html", "popup.html",
"template-renderer.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 *"
} }

View File

@ -123,6 +123,7 @@
<script src="/js/language/text-scanner.js"></script> <script src="/js/language/text-scanner.js"></script>
<script src="/js/media/audio-system.js"></script> <script src="/js/media/audio-system.js"></script>
<script src="/js/media/media-loader.js"></script> <script src="/js/media/media-loader.js"></script>
<script src="/js/media/media-util.js"></script>
<script src="/js/media/text-to-speech-audio.js"></script> <script src="/js/media/text-to-speech-audio.js"></script>
<script src="/js/script/dynamic-loader.js"></script> <script src="/js/script/dynamic-loader.js"></script>
<script src="/js/templates/template-renderer-proxy.js"></script> <script src="/js/templates/template-renderer-proxy.js"></script>

View File

@ -107,6 +107,7 @@
<script src="/js/language/text-scanner.js"></script> <script src="/js/language/text-scanner.js"></script>
<script src="/js/media/audio-system.js"></script> <script src="/js/media/audio-system.js"></script>
<script src="/js/media/media-loader.js"></script> <script src="/js/media/media-loader.js"></script>
<script src="/js/media/media-util.js"></script>
<script src="/js/media/text-to-speech-audio.js"></script> <script src="/js/media/text-to-speech-audio.js"></script>
<script src="/js/script/dynamic-loader.js"></script> <script src="/js/script/dynamic-loader.js"></script>
<script src="/js/templates/template-renderer-proxy.js"></script> <script src="/js/templates/template-renderer-proxy.js"></script>