From 8aeb670be1d252807252ee2bfe99b15b81c3e28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs?= Date: Wed, 6 Apr 2022 10:38:06 +0800 Subject: update from upstream --- js/about.js | 2 +- js/asset-viewer.js | 8 +- js/assets.js | 1744 ++++++++++++++++++++++----------------------- js/background.js | 170 ++--- js/browsercache.js | 34 +- js/cloud-ui.js | 132 ++-- js/contentscript-start.js | 20 +- js/contentscript.js | 558 +++++++-------- js/cookies.js | 374 +++++----- js/dashboard-common.js | 8 +- js/dashboard.js | 6 +- js/hosts-files.js | 340 ++++----- js/httpsb.js | 4 +- js/i18n.js | 208 +++--- js/liquid-dict.js | 314 ++++---- js/logger-ui.js | 798 ++++++++++----------- js/logger.js | 10 +- js/main-blocked.js | 2 +- js/matrix.js | 1394 ++++++++++++++++++------------------ js/messaging.js | 24 +- js/pagestats.js | 186 ++--- js/polyfill.js | 4 +- js/popup.js | 1572 ++++++++++++++++++++-------------------- js/profiler.js | 4 +- js/raw-settings.js | 144 ++-- js/settings.js | 124 ++-- js/start.js | 70 +- js/storage.js | 14 +- js/tab.js | 562 +++++++-------- js/traffic.js | 4 +- js/udom.js | 7 +- js/uritools.js | 886 +++++++++++------------ js/user-rules.js | 308 ++++---- js/usersettings.js | 4 +- js/utils.js | 4 +- js/vapi-background.js | 1241 ++++++++++++++------------------ js/vapi-browser.js | 244 +++---- js/vapi-client.js | 170 ++--- js/vapi-cloud.js | 110 +-- js/vapi-common.js | 142 ++-- js/vapi-contextmenu.js | 182 ++--- js/vapi-cookies.js | 96 +-- js/vapi-core.js | 114 +-- js/vapi-messaging.js | 132 ++-- js/vapi-net.js | 50 +- js/vapi-popup.js | 4 +- js/vapi-storage.js | 260 +++---- js/vapi-tabs.js | 666 ++++++++--------- js/vapi-window.js | 170 ++--- js/xal.js | 60 +- 50 files changed, 6725 insertions(+), 6959 deletions(-) (limited to 'js') diff --git a/js/about.js b/js/about.js index a35b45c..9ada80e 100644 --- a/js/about.js +++ b/js/about.js @@ -2,7 +2,7 @@ ηMatrix - a browser extension to black/white list requests. Copyright (C) 2014-2019 Raymond Hill - Copyright (C) 2019-2020 Alessio Vanni + Copyright (C) 2019-2022 Alessio Vanni This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/js/asset-viewer.js b/js/asset-viewer.js index 8998892..12f0e3b 100644 --- a/js/asset-viewer.js +++ b/js/asset-viewer.js @@ -2,7 +2,7 @@ ηMatrix - a browser extension to black/white list requests. Copyright (C) 2014-2019 Raymond Hill - Copyright (C) 2019-2020 Alessio Vanni + Copyright (C) 2019-2022 Alessio Vanni This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see {http://www.gnu.org/licenses/}. - Home: https://libregit.spks.xyz/heckyel/ematrix + Home: https://gitlab.com/vannilla/ematrix uMatrix Home: https://github.com/gorhill/uMatrix */ @@ -36,7 +36,7 @@ } vAPI.messaging.send('asset-viewer.js', { - what : 'getAssetContent', - url: matches[1] + what : 'getAssetContent', + url: matches[1] }, onAssetContentReceived); })(); diff --git a/js/assets.js b/js/assets.js index 286cf0d..9bb2470 100644 --- a/js/assets.js +++ b/js/assets.js @@ -2,7 +2,7 @@ ηMatrix - a browser extension to black/white list requests. Copyright (C) 2013-2019 Raymond Hill - Copyright (C) 2019-2020 Alessio Vanni + Copyright (C) 2019-2022 Alessio Vanni This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,14 +17,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see {http://www.gnu.org/licenses/}. - Home: https://libregit.spks.xyz/heckyel/ematrix + Home: https://gitlab.com/vannilla/ematrix uMatrix Home: https://github.com/gorhill/uMatrix */ 'use strict'; -/******************************************************************************/ - ηMatrix.assets = (function() { let api = {}; @@ -33,21 +31,21 @@ let connectionError = vAPI.i18n('errorCantConnectTo'); let notifyObservers = function (topic, details) { - let result; + let result; - for (let i=0; i 0) { - vAPI.cacheStorage.remove(removedContent); + if (contents.length !== 0) { + vAPI.cacheStorage.remove(contents); - let bin = { - assetCacheRegistry: cacheRegistry, - }; + let bin = { + assetCacheRegistry: cache, + }; + vAPI.cacheStorage.set(bin); + } - vAPI.cacheStorage.set(bin); - } + if (typeof callback === 'function') { + callback(); + } - if (typeof callback === 'function') { - callback(); - } + for (let i=0; i now) { - continue; - } - if (notifyObservers('before-asset-updated', {assetKey: key})) { - return key; - } - - gcOne(key); - } - - return undefined; - }; - - let onUpdate = function (details) { - if (details.content !== '') { - updated.push(details.assetKey); - if (details.assetKey === 'assets.json') { - updateSourceRegistry(details.content); - } - } else { - notifyObservers('asset-update-failed', { - assetKey: details.assetKey, - }); - } - - if (findOne() !== undefined) { - vAPI.setTimeout(updateNext, updateDelay); - } else { - updateEnd(); - } - }; - - let updateOne = function () { - let key = findOne(); - if (key === undefined) { - updateEnd(); - return; - } - - updateFetch.add(key); - getRemote(key, onUpdate); - }; - - let onSourceReady = function (registry) { - sourceReg = registry; - updateOne(); - }; - - let onCacheReady = function (registry) { - cacheReg = registry; - getSourceRegistry(onSourceReady); - }; - - getCacheRegistry(onCacheReady); + let cacheReg = undefined; + let sourceReg = undefined; + + let gcOne = function (key) { + let entry = cacheReg[key]; + if (entry && entry.readTime < cacheRegistryStart) { + removeCache(key); + } + }; + + let findOne = function () { + let now = Date.now(); + let sourceEntry; + let cacheEntry; + + for (let key in sourceReg) { + sourceEntry = sourceReg[key]; + if (sourceEntry.hasRemoteURL !== true) { + continue; + } + if (updateFetch.has(key)) { + continue; + } + + cacheEntry = cacheReg[key]; + if (cacheEntry + && (cacheEntry.writeTime + + sourceEntry.updateAfter*86400000) > now) { + continue; + } + if (notifyObservers('before-asset-updated', {assetKey: key})) { + return key; + } + + gcOne(key); + } + + return undefined; + }; + + let onUpdate = function (details) { + if (details.content !== '') { + updated.push(details.assetKey); + if (details.assetKey === 'assets.json') { + updateSourceRegistry(details.content); + } + } else { + notifyObservers('asset-update-failed', { + assetKey: details.assetKey, + }); + } + + if (findOne() !== undefined) { + vAPI.setTimeout(updateNext, updateDelay); + } else { + updateEnd(); + } + }; + + let updateOne = function () { + let key = findOne(); + if (key === undefined) { + updateEnd(); + return; + } + + updateFetch.add(key); + getRemote(key, onUpdate); + }; + + let onSourceReady = function (registry) { + sourceReg = registry; + updateOne(); + }; + + let onCacheReady = function (registry) { + cacheReg = registry; + getSourceRegistry(onSourceReady); + }; + + getCacheRegistry(onCacheReady); }; let updateEnd = function () { - let keys = updated.slice(0); - updateFetch.clear(); - updateStatus = 'stop'; - updateDelay = updateDefaultDelay; - notifyObservers('after-asset-updated', { - assetKeys: keys, - }); + let keys = updated.slice(0); + updateFetch.clear(); + updateStatus = 'stop'; + updateDelay = updateDefaultDelay; + notifyObservers('after-asset-updated', { + assetKeys: keys, + }); }; // Assets API api.addObserver = function (observer) { - if (observers.indexOf(observer) === -1) { - observers.push(observer); - } + if (observers.indexOf(observer) === -1) { + observers.push(observer); + } }; api.removeObserver = function (observer) { - let pos = observers.indexOf(observer); - if (pos !== -1) { - observers.splice(pos, 1); - } + let pos = observers.indexOf(observer); + while (pos !== -1) { + observers.splice(pos, 1); + pos = observers.indexOf(observer); + } }; api.fetchText = function (url, onLoad, onError, tries) { - let iurl = externalPathRegex.test(url) ? url : vAPI.getURL(url); - let tr = (tries === undefined) ? 10 : tries; - - if (typeof onError !== 'function') { - onError = onLoad; - } - - let onResponseReceived = function () { - this.onload = this.onerror = this.ontimeout = null; - - let details = { - url: url, - content: '', - // On local files this.status is 0, but the request - // is successful - statusCode: this.status || 200, - statusText: this.statusText || '', - }; - - if (details.statusCode < 200 || details.statusCode >= 300) { - onError.call(null, details, tr); - return; - } - - if (isEmptyString(this.responseText) === true) { - onError.call(null, details, tr); - return; - } - - let t = this.responseText.trim(); - - // Discard HTML as it's probably an error - // (the request expects plain text as a response) - if (t.startsWith('<') && t.endsWith('>')) { - onError.call(null, details, tr); - return; - } - - details.content = t; - onLoad.call(null, details, tr); - }; - - let onErrorReceived = function () { - this.onload = this.onerror = this.ontimeout = null; - - ηMatrix.logger.writeOne('', 'error', - connectionError.replace('{{url}}', iurl)); - - onError.call(null, { - url: url, - content: '', - }, tr); - }; - - let req = new XMLHttpRequest(); - req.open('GET', iurl, true); - req.timeout = 30000; - req.onload = onResponseReceived; - req.onerror = onErrorReceived; - req.ontimeout = onErrorReceived; - req.responseType = 'text'; - - try { - // This can throw in some cases - req.send(); - } catch (e) { - onErrorReceived.call(req); - } + let iurl = externalPathRegex.test(url) ? url : vAPI.getURL(url); + let tr = (tries === undefined) ? 10 : tries; + + if (typeof onError !== 'function') { + onError = onLoad; + } + + let onResponseReceived = function () { + this.onload = this.onerror = this.ontimeout = null; + + let details = { + url: url, + content: '', + // On local files this.status is 0, but the request + // is successful + statusCode: this.status || 200, + statusText: this.statusText || '', + }; + + if (details.statusCode < 200 || details.statusCode >= 300) { + return onError.call(null, details, tr); + } + + if (isEmptyString(this.responseText) === true) { + return onError.call(null, details, tr); + } + + let t = this.responseText.trim(); + + // Discard HTML as it's probably an error + // (the request expects plain text as a response) + if (t.startsWith('<') && t.endsWith('>')) { + return onError.call(null, details, tr); + } + + details.content = t; + return onLoad.call(null, details, tr); + }; + + let onErrorReceived = function () { + this.onload = this.onerror = this.ontimeout = null; + + ηMatrix.logger.writeOne('', 'error', + connectionError.replace('{{url}}', iurl)); + + onError.call(null, { + url: url, + content: '', + }, tr); + }; + + let req = new XMLHttpRequest(); + req.open('GET', iurl, true); + req.timeout = 30000; + req.onload = onResponseReceived; + req.onerror = onErrorReceived; + req.ontimeout = onErrorReceived; + req.responseType = 'text'; + + try { + // This can throw in some cases + req.send(); + } catch (e) { + onErrorReceived.call(req); + } }; api.registerAssetSource = function (key, details) { - getSourceRegistry(function () { - registerSource(key, details); - saveSourceRegistry(true); - }); + getSourceRegistry(function () { + registerSource(key, details); + saveSourceRegistry(true); + }); }; api.unregisterAssetSource = function (key) { - getSourceRegistry(function () { - unregisterSource(key); - saveSourceRegistry(true); - }); + getSourceRegistry(function () { + unregisterSource(key); + saveSourceRegistry(true); + }); }; api.get = function (key, options, callback) { - let cb; - let opt; - - if (typeof options === 'function') { - cb = options; - opt = {}; - } else if (typeof callback !== 'function') { - cb = noOp; - opt = options; - } else { - cb = callback; - opt = options; - } - - let assetDetails = {}; - let contentUrl = undefined; - - let report = function (content, error) { - let details = { - assetKey: key, - content: content, - }; - - if (error) { - details.error = assetDetails.error = error; - } else { - assetDetails.error = undefined; - } - - cb(details); - }; - - let onContentNotLoaded = function (details, tries) { - let external; - let urls = []; - - let tr = (tries === undefined) ? 10 : tries; - - if (tr <= 0) { - console.warn('ηMatrix couldn\'t download the asset ' - +assetDetails.title); - return; - } - - if (typeof assetDetails.contentURL === 'string') { - urls = [assetDetails.contentURL]; - } else if (Array.isArray(assetDetails.contentURL)) { - urls = assetDetails.contentURL.slice(0); - } - - while ((contentUrl = urls.shift())) { - external = externalPathRegex.test(contentUrl); - if (external === true && assetDetails.loaded !== true) { - break; - } - if (external === false || assetDetails.hasLocalURL !== true) { - break; - } - } - - if (!contentUrl) { - report('', 'E_NOTFOUND'); - return; - } - - api.fetchText(contentUrl, onContentLoaded, onContentNotLoaded, - tr-1); - }; - - let onContentLoaded = function (details, tries) { - if (isEmptyString(details.content) === true) { - onContentNotLoaded(undefined, tries); - return; - } - - if (externalPathRegex.test(details.url) - && opt.dontCache !== true) { - writeCache(key, { - content: details.content, - url: contentUrl, - }); - } - - assetDetails.loaded = true; - - report(details.content); - }; - - let onCachedContentLoad = function (details) { - if (details.content !== '') { - report(details.content); - return; - } - - let onReady = function (registry) { - assetDetails = registry[key] || {}; - onContentNotLoaded(); - } - - getSourceRegistry(onReady); - }; - - readCache(key, onCachedContentLoad); + let cb; + let opt; + + if (typeof options === 'function') { + cb = options; + opt = {}; + } else if (typeof callback !== 'function') { + cb = noOp; + opt = options; + } else { + cb = callback; + opt = options; + } + + let assetDetails = {}; + let urls = []; + let contentUrl = ''; + + let report = function (content, error) { + let details = { + assetKey: key, + content: content, + }; + + if (error) { + details.error = assetDetails.error = error; + } else { + assetDetails.error = undefined; + } + + cb(details); + }; + + let onContentNotLoaded = function (details, tries) { + let external; + + let tr = (tries === undefined) ? 10 : tries; + + if (tr <= 0) { + console.warn('ηMatrix couldn\'t download the asset ' + +assetDetails.title); + return; + } + + while ((contentUrl = urls.shift())) { + external = externalPathRegex.test(contentUrl); + if (external === true && assetDetails.loaded !== true) { + break; + } + if (external === false || assetDetails.hasLocalURL !== true) { + break; + } + } + + if (!contentUrl) { + return report('', 'E_NOTFOUND'); + } + + api.fetchText(contentUrl, + onContentLoaded, + onContentNotLoaded, + tr-1); + }; + + let onContentLoaded = function (details, tries) { + if (isEmptyString(details.content) === true) { + return onContentNotLoaded(undefined, tries); + } + + if (externalPathRegex.test(details.url) + && opt.dontCache !== true) { + writeCache(key, { + content: details.content, + url: contentUrl, + }); + } + + assetDetails.loaded = true; + + report(details.content); + }; + + let onCachedContentLoad = function (details) { + if (details.content !== '') { + return report(details.content); + } + + let onRead = function (details) { + console.debug(details); + report(details.content || 'missing contents'); + } + + let onReady = function (registry) { + assetDetails = registry[key] || {}; + if (typeof assetDetails.contentURL === 'string') { + urls = [assetDetails.contentURL]; + } else if (Array.isArray(assetDetails.contentURL)) { + urls = assetDetails.contentURL.slice(0); + } + if (true === assetDetails.loaded) { + readCache(key, onRead); + } else { + onContentNotLoaded(); + } + } + + getSourceRegistry(onReady); + }; + + readCache(key, onCachedContentLoad); }; api.put = function (key, content, callback) { - writeCache(key, content, callback); + writeCache(key, content, callback); }; api.metadata = function (callback) { - let onSourceReady = function (registry) { - let source = JSON.parse(JSON.stringify(registry)); - let cache = cacheRegistry; - let sourceEntry; - let cacheEntry; - let now = Date.now(); - let obsoleteAfter; - - for (let key in source) { - sourceEntry = source[key]; - cacheEntry = cache[key]; - - if (cacheEntry) { - sourceEntry.cached = true; - sourceEntry.writeTime = cacheEntry.writeTime; - obsoleteAfter = cacheEntry.writeTime - + sourceEntry.updateAfter * 86400000; - sourceEntry.obsolete = obsoleteAfter < now; - sourceEntry.remoteURL = cacheEntry.remoteURL; - } else { - sourceEntry.writeTime = 0; - obsoleteAfter = 0; - sourceEntry.obsolete = true; - } - } - - callback(source); - } - - let onCacheReady = function () { - getSourceRegistry(onSourceReady); - } - - getCacheRegistry(onCacheReady); + let onSourceReady = function (registry) { + let source = JSON.parse(JSON.stringify(registry)); + let cache = cacheRegistry; + let sourceEntry; + let cacheEntry; + let now = Date.now(); + let obsoleteAfter; + + for (let key in source) { + sourceEntry = source[key]; + cacheEntry = cache[key]; + + if (cacheEntry) { + sourceEntry.cached = true; + sourceEntry.writeTime = cacheEntry.writeTime; + obsoleteAfter = cacheEntry.writeTime + + sourceEntry.updateAfter * 86400000; + sourceEntry.obsolete = obsoleteAfter < now; + sourceEntry.remoteURL = cacheEntry.remoteURL; + } else { + sourceEntry.writeTime = 0; + obsoleteAfter = 0; + sourceEntry.obsolete = true; + } + } + + callback(source); + } + + let onCacheReady = function () { + getSourceRegistry(onSourceReady); + } + + getCacheRegistry(onCacheReady); }; api.purge = function (pattern, exclude, callback) { - markDirtyCache(pattern, exclude, callback); + markDirtyCache(pattern, exclude, callback); }; api.remove = function (pattern, callback) { - cacheRemove(pattern, callback); + removeCache(pattern, callback); }; api.rmrf = function () { - cacheRemove(/./); + removeCache(/./); }; api.updateStart = function (details) { - let oldDelay = updateDelay; - let newDelay = details.delay || updateDefaultDelay; + let oldDelay = updateDelay; + let newDelay = details.delay || updateDefaultDelay; - updateDelay = Math.min(oldDelay, newDelay); + updateDelay = Math.min(oldDelay, newDelay); - if (updateStatus !== undefined) { - if (newDelay < oldDelay) { - clearTimeout(updateTimer); - updateTimer = vAPI.setTimeout(updateNext, updateDelay); - } - return; - } + if (updateStatus !== 'stop') { + if (newDelay < oldDelay) { + clearTimeout(updateTimer); + updateTimer = vAPI.setTimeout(updateNext, updateDelay); + } + return; + } - updateStart(); + updateStart(); }; api.updateStop = function () { - if (updateTimer) { - clearTimeout(updateTimer); - updateTimer = undefined; - } - if (updateStatus === 'running') { - updateEnd(); - } + if (updateTimer) { + clearTimeout(updateTimer); + updateTimer = undefined; + } + if (updateStatus === 'running') { + updateEnd(); + } }; api.checkVersion = function () { - let cache; - - let onSourceReady = function (registry) { - let source = JSON.parse(JSON.stringify(registry)); - let version = ηMatrix.userSettings.assetVersion; - - if (!version) { - ηMatrix.userSettings.assetVersion = 1; - version = 1; - } - - if (!source["assets.json"].version - || version > source["assets.json"].version) { - for (let key in source) { - switch (key) { - case "hphosts": - case "malware-0": - case "malware-1": - delete source[key]; - api.remove(key, function () {}); - break; - default: - break; - } - - source["assets.json"].version = version; - } - - let createRegistry = function () { - api.fetchText - (ηMatrix.assetsBootstrapLocation || 'assets/assets.json', - function (details) { - updateSourceRegistry(details.content, true); - }); - }; - - createRegistry(); - } - }; - - let onCacheReady = function (registry) { - cache = JSON.parse(JSON.stringify(registry)); - - getSourceRegistry(onSourceReady); - }; - - getCacheRegistry(onCacheReady); + let cache; + + let onSourceReady = function (registry) { + let source = JSON.parse(JSON.stringify(registry)); + let version = ηMatrix.userSettings.assetVersion; + + if (!version) { + ηMatrix.userSettings.assetVersion = 1; + version = 1; + } + + if (!source["assets.json"].version + || version > source["assets.json"].version) { + for (let key in source) { + switch (key) { + case "hphosts": + case "malware-0": + case "malware-1": + delete source[key]; + api.remove(key, function () {}); + break; + default: + break; + } + + source["assets.json"].version = version; + } + + let createRegistry = function () { + api.fetchText + (ηMatrix.assetsBootstrapLocation || 'assets/assets.json', + function (details) { + updateSourceRegistry(details.content, true); + }); + }; + + createRegistry(); + } + }; + + let onCacheReady = function (registry) { + cache = JSON.parse(JSON.stringify(registry)); + + getSourceRegistry(onSourceReady); + }; + + getCacheRegistry(onCacheReady); }; return api; })(); - -/******************************************************************************/ diff --git a/js/background.js b/js/background.js index 889decd..b31bbed 100644 --- a/js/background.js +++ b/js/background.js @@ -2,7 +2,7 @@ ηMatrix - a browser extension to black/white list requests. Copyright (C) 2014-2019 Raymond Hill - Copyright (C) 2019-2020 Alessio Vanni + Copyright (C) 2019-2022 Alessio Vanni This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see {http://www.gnu.org/licenses/}. - Home: https://libregit.spks.xyz/heckyel/ematrix + Home: https://gitlab.com/vannilla/ematrix uMatrix Home: https://github.com/gorhill/uMatrix */ @@ -32,45 +32,45 @@ var ηMatrix = (function () { let oneDay = 24 * oneHour; let _RequestStats = function () { - this.reset(); + this.reset(); }; _RequestStats.prototype.reset = function () { - this.all = - this.doc = - this.frame = - this.script = - this.css = - this.image = - this.media = - this.xhr = - this.other = - this.cookie = 0; + this.all = + this.doc = + this.frame = + this.script = + this.css = + this.image = + this.media = + this.xhr = + this.other = + this.cookie = 0; }; var RequestStats = function () { - this.allowed = new _RequestStats (); - this.blocked = new _RequestStats (); + this.allowed = new _RequestStats (); + this.blocked = new _RequestStats (); }; RequestStats.prototype.reset = function () { - this.blocked.reset(); - this.allowed.reset(); + this.blocked.reset(); + this.allowed.reset(); }; RequestStats.prototype.record = function (type, blocked) { - // Remember: always test against **false** - if (blocked !== false) { + // Remember: always test against **false** + if (blocked !== false) { this.blocked[type] += 1; this.blocked.all += 1; - } else { + } else { this.allowed[type] += 1; this.allowed.all += 1; - } + } }; var requestStatsFactory = function () { - return new RequestStats(); + return new RequestStats(); }; /** @@ -93,8 +93,8 @@ var ηMatrix = (function () { */ var rawSettingsDefault = { - disableCSPReportInjection: false, - placeholderBackground: [ + disableCSPReportInjection: false, + placeholderBackground: [ 'url("data:image/png;base64,', 'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAK', 'CAAAAACoWZBhAAAABGdBTUEAALGPC/xh', @@ -108,13 +108,13 @@ var ηMatrix = (function () { 'KzAzOjAwa+9TNQAAAABJRU5ErkJggg==', '") ', 'repeat scroll #fff' - ].join(''), - placeholderBorder: '1px solid rgba(0, 0, 0, 0.1)', - imagePlaceholder: true, - imagePlaceholderBackground: 'default', - imagePlaceholderBorder: 'default', - framePlaceholder: true, - framePlaceholderDocument: [ + ].join(''), + placeholderBorder: '1px solid rgba(0, 0, 0, 0.1)', + imagePlaceholder: true, + imagePlaceholderBackground: 'default', + imagePlaceholderBorder: 'default', + framePlaceholder: true, + framePlaceholderDocument: [ '', '', '