diff --git a/.addonslinterignore b/.addonslinterignore index 7812efbe..201a8507 100644 --- a/.addonslinterignore +++ b/.addonslinterignore @@ -5,3 +5,4 @@ # Setting innerHTML in rester-dom-purify-frame.html should be fine because HTML # is sanitized with DOMPurify. site/scripts/bundle.js UNSAFE_VAR_ASSIGNMENT + JSON_INVALID "/manifest_version" must be <= 2 diff --git a/package.json b/package.json index 74a8257c..42a7fbd4 100644 --- a/package.json +++ b/package.json @@ -8,15 +8,15 @@ "author": "Jan Kühle (https://kuehle.me)", "repository": "frigus02/RESTer", "scripts": { - "start": "yarn run clean:build && concurrently \"yarn run build:files -w\" \"yarn run webpack --progress --mode development --devtool source-map --watch\"", + "start": "yarn run clean:build && concurrently \"yarn run build:files development\" \"yarn run webpack --progress --mode development --devtool source-map --watch\"", "clean:build": "rimraf build", "clean:package": "rimraf package", - "build": "yarn run clean:build && yarn run build:files && yarn run build:site", - "build:files": "cpx \"src/{images/**,manifest.json}\" build/", + "build": "yarn run clean:build && yarn run build:files production && yarn run build:site", + "build:files": "node tools/scripts/copy-manifest-and-icons.mjs", "build:site": "webpack --mode production", "lint": "yarn run lint:javascript && yarn run lint:firefox", "lint:javascript": "eslint .", - "lint:firefox": "node tools/scripts/lint-firefox-addon.mjs build/", + "lint:firefox": "node tools/scripts/lint-firefox-addon.mjs build/firefox/", "format": "prettier --write \"**/*.{js,mjs,md}\" !build/**", "test": "jest --env jsdom \"src/.+\\.test\\.js$\"", "test:e2e": "jest \"test-e2e/.+\\.test\\.js$\"", @@ -77,7 +77,6 @@ "chalk": "5.2.0", "concurrently": "7.6.0", "copy-webpack-plugin": "11.0.0", - "cpx2": "4.2.0", "eslint": "8.36.0", "eslint-config-crockford": "2.0.0", "eslint-config-prettier": "8.7.0", diff --git a/src/background/data/utils/data-store.js b/src/background/data/utils/data-store.js index e5d5674d..c127005e 100644 --- a/src/background/data/utils/data-store.js +++ b/src/background/data/utils/data-store.js @@ -61,7 +61,7 @@ class DataStore extends CustomEventTarget { const actions = {}; ['add', 'put', 'delete'].forEach((action) => { - actions[action] = function (tableName, entity) { + actions[action] = function(tableName, entity) { if (!queue[tableName]) { queue[tableName] = []; } @@ -72,7 +72,7 @@ class DataStore extends CustomEventTarget { }; }); - actions.execute = function () { + actions.execute = function() { return dataStore._withWriteLock((changes) => { const result = []; @@ -335,33 +335,26 @@ class DataStore extends CustomEventTarget { return this._performStorageLocalOperation('remove', keys); } - _performStorageLocalOperation( + async _performStorageLocalOperation( op, keys, transformSuccessResult = (arg) => arg ) { - return new Promise((resolve, reject) => { - const start = performance.now(); - chrome.storage.local[op](keys, (result) => { - const millis = performance.now() - start; - if (millis > 500) { - this.dispatchEvent( - new CustomEvent('slowPerformance', { - detail: { - operation: `storage.local.${op}(...)`, - duration: Math.round(millis), - }, - }) - ); - } + const start = performance.now(); + const result = await chrome.storage.local[op](keys); + const millis = performance.now() - start; + if (millis > 500) { + this.dispatchEvent( + new CustomEvent('slowPerformance', { + detail: { + operation: `storage.local.${op}(...)`, + duration: Math.round(millis), + }, + }) + ); + } - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError); - } else { - resolve(transformSuccessResult(result)); - } - }); - }); + return transformSuccessResult(result); } } diff --git a/src/background/index.js b/src/background/index.js index 0e72984a..72900b56 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -8,8 +8,6 @@ import * as exportImport from './exportImport/index.js'; import * as settings from './settings/index.js'; import { select } from './utils/fields.js'; -// WARNING: All properties must be in quotes. Otherwise UglifyJS would ufligy -// them and they wouldn't be reachable by name. const resterApi = { data: { authorizationProviderConfigurations: { @@ -52,29 +50,28 @@ const resterApi = { }, }; -chrome.browserAction.onClicked.addListener(() => { - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - const resterUrl = chrome.runtime.getURL('site/index.html'); - const blankUrls = ['about:blank', 'about:newtab']; - if (blankUrls.includes(tabs[0].url)) { - try { - chrome.tabs.update({ - loadReplace: true, - url: resterUrl, - }); - } catch (e) { - // Chrome does not support loadReplace and throws an exception, - // it is specified. Try again without loadReplace. - chrome.tabs.update({ - url: resterUrl, - }); - } - } else { - chrome.tabs.create({ +chrome.action.onClicked.addListener(async () => { + const tabs = await chrome.tabs.query({ active: true, currentWindow: true }) + const resterUrl = chrome.runtime.getURL('site/index.html'); + const blankUrls = ['about:blank', 'about:newtab']; + if (blankUrls.includes(tabs[0].url)) { + try { + chrome.tabs.update({ + loadReplace: true, + url: resterUrl, + }); + } catch (e) { + // Chrome does not support loadReplace and throws an exception, + // it is specified. Try again without loadReplace. + chrome.tabs.update({ url: resterUrl, }); } - }); + } else { + chrome.tabs.create({ + url: resterUrl, + }); + } }); chrome.runtime.onConnect.addListener((port) => { diff --git a/src/background/settings/index.js b/src/background/settings/index.js index c2cbb34c..607510c8 100644 --- a/src/background/settings/index.js +++ b/src/background/settings/index.js @@ -15,28 +15,13 @@ const DEFAULTS = { theme: 'dark', }; -function getSettings() { - return new Promise((resolve, reject) => { - chrome.storage.local.get('settings', (result) => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError); - } else { - resolve(result.settings || {}); - } - }); - }); +async function getSettings() { + const result = await chrome.storage.local.get('settings'); + return result.settings || {}; } -function setSettings(settings) { - return new Promise((resolve, reject) => { - chrome.storage.local.set({ settings }, () => { - if (chrome.runtime.lastError) { - reject(chrome.runtime.lastError); - } else { - resolve(); - } - }); - }); +async function setSettings(settings) { + await chrome.storage.local.set({ settings }); } export const e = new CustomEventTarget(); diff --git a/src/manifest-chrome.json b/src/manifest-chrome.json new file mode 100644 index 00000000..0c9584ca --- /dev/null +++ b/src/manifest-chrome.json @@ -0,0 +1,37 @@ +{ + "manifest_version": 3, + "name": "RESTer", + "version": "4.11.0", + "description": "A REST client for almost any web service.", + "author": "Jan Kühle", + "homepage_url": "https://github.com/frigus02/RESTer", + "icons": { + "48": "images/icon-dev48.png", + "128": "images/icon-dev128.png" + }, + "permissions": [ + "activeTab", + "downloads", + "storage", + "unlimitedStorage", + "declarativeNetRequest", + "declarativeNetRequestWithHostAccess" + ], + "optional_permissions": [ + "cookies" + ], + "host_permissions": [ + "" + ], + "background": { + "service_worker": "background/bundle.js" + }, + "action": { + "default_icon": { + "16": "images/icon-dev16.png", + "24": "images/icon-dev24.png", + "32": "images/icon-dev32.png" + } + }, + "minimum_chrome_version": "111" +} diff --git a/src/manifest.json b/src/manifest-firefox.json similarity index 80% rename from src/manifest.json rename to src/manifest-firefox.json index 61e3a017..5a38d2ba 100644 --- a/src/manifest.json +++ b/src/manifest-firefox.json @@ -1,5 +1,5 @@ { - "manifest_version": 2, + "manifest_version": 3, "name": "RESTer", "version": "4.11.0", "description": "A REST client for almost any web service.", @@ -13,20 +13,20 @@ "activeTab", "downloads", "storage", - "unlimitedStorage", - "" + "unlimitedStorage" ], "optional_permissions": [ "cookies", "webRequest", "webRequestBlocking" ], + "host_permissions": [ + "" + ], "background": { - "scripts": [ - "background/bundle.js" - ] + "scripts": ["background/bundle.js"] }, - "browser_action": { + "action": { "default_icon": { "16": "images/icon-dev16.png", "24": "images/icon-dev24.png", @@ -49,5 +49,11 @@ "size": 32 } ] + }, + "browser_specific_settings": { + "gecko": { + "id": "rester@kuehle.me", + "strict_min_version": "110.0" + } } } diff --git a/src/shared/util.js b/src/shared/util.js index f10caac4..a3a1d6cc 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -247,3 +247,40 @@ export function parseStatusLine(statusLine) { const reasonPhrase = statusLine.substring(secondSpace + 1); return { httpVersion, statusCode, reasonPhrase }; } + +export function getFilenameFromContentDispositionHeader(disposition) { + const utf8FilenameRegex = /filename\*=UTF-8''([\w%\-.]+)(?:; ?|$)/i; + const asciiFilenameRegex = /filename=(["']?)(.*?[^\\])\1(?:; ?|$)/i; + + let fileName = null; + if (utf8FilenameRegex.test(disposition)) { + fileName = decodeURIComponent(utf8FilenameRegex.exec(disposition)[1]); + } else { + // Prevent ReDos attacks by anchoring the ascii regex to string start + // and slicing off everything before 'filename=' + const filenameStart = disposition.toLowerCase().indexOf('filename='); + if (filenameStart >= 0) { + const partialDisposition = disposition.slice(filenameStart); + const matches = asciiFilenameRegex.exec(partialDisposition); + if (matches !== null && matches[2]) { + fileName = matches[2]; + } + } + } + + if (fileName !== null) { + // Sanitize filename for illegal characters + const illegalRe = /[/?<>\\:*|":]/g; + const controlRe = /[\x00-\x1f\x80-\x9f]/g; + const reservedRe = /^\.+/g; + const windowsReservedRe = + /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; + fileName = fileName + .replace(illegalRe, '') + .replace(controlRe, '') + .replace(reservedRe, '') + .replace(windowsReservedRe, ''); + } + + return fileName; +} diff --git a/src/shared/util.test.js b/src/shared/util.test.js index aa8ea8fa..32624c2a 100644 --- a/src/shared/util.test.js +++ b/src/shared/util.test.js @@ -11,6 +11,7 @@ import { mergeCookies, parseMediaType, parseStatusLine, + getFilenameFromContentDispositionHeader, } from './util.js'; describe('clone', function () { @@ -215,3 +216,29 @@ describe('parseStatusLine', function () { }); }); }); + +describe('getFilenameFromContentDispositionHeader', function () { + test('utf8', function () { + expect( + getFilenameFromContentDispositionHeader( + "attachment;filename*=UTF-8''%6A%C5%AF%C5%AF%C5%AF%C5%BE%C4%9B%2E%74%78%74" + ) + ).toEqual('jůůůžě.txt'); + }); + + test('ascii', function () { + expect( + getFilenameFromContentDispositionHeader( + 'attachment; filename="cool.html"' + ) + ).toEqual('cool.html'); + }); + + test('reverved characters', function () { + expect( + getFilenameFromContentDispositionHeader( + 'attachment; filename="../c|o?ol.html"' + ) + ).toEqual('cool.html'); + }); +}); diff --git a/src/site/elements/controls/rester-authorization-provider-cookie.js b/src/site/elements/controls/rester-authorization-provider-cookie.js index 15de9866..3392817b 100644 --- a/src/site/elements/controls/rester-authorization-provider-cookie.js +++ b/src/site/elements/controls/rester-authorization-provider-cookie.js @@ -9,17 +9,12 @@ async function ensureCookiesPermission() { permissions: ['cookies'], }; - return new Promise((resolve, reject) => { - chrome.permissions.request(requiredPermissions, (result) => { - if (result) { - resolve(); - } else { - reject( - 'RESTer needs the permissions to read cookies for this.' - ); - } - }); - }); + const result = await chrome.permissions.request(requiredPermissions); + if (!result) { + throw new Error( + 'RESTer needs the permissions to read cookies for this.' + ); + } } function filterCookies(cookies, cookieNames) { diff --git a/src/site/elements/controls/rester-variables-input.js b/src/site/elements/controls/rester-variables-input.js index 036a8f71..c2d8dd4d 100644 --- a/src/site/elements/controls/rester-variables-input.js +++ b/src/site/elements/controls/rester-variables-input.js @@ -104,6 +104,7 @@ class RESTerVariablesInput extends RESTerVariablesMixin(PolymerElement) { constructor() { super(); + this._initVariableHistory = this._initVariableHistory.bind(this); this._onDataChanges = this._onDataChanges.bind(this); this._updateVariables = this._updateVariables.bind(this); } @@ -113,20 +114,22 @@ class RESTerVariablesInput extends RESTerVariablesMixin(PolymerElement) { this._setLastUsedVariables({}); this._setVariableHistory({}); - - getHistoryEntries(25, ['request.variables.values']).then((entries) => { - entries.reverse(); - for (let entry of entries) { - this._addHistoryEntryToVariableHistory(entry); - } - }); + this._initVariableHistory(); resterEvents.addEventListener('dataChange', this._onDataChanges); + resterEvents.addEventListener( + 'connectionReset', + this._initVariableHistory + ); } disconnectedCallback() { super.disconnectedCallback(); resterEvents.removeEventListener('dataChange', this._onDataChanges); + resterEvents.removeEventListener( + 'connectionReset', + this._initVariableHistory + ); } _updateVariablesDebounced() { @@ -191,6 +194,15 @@ class RESTerVariablesInput extends RESTerVariablesMixin(PolymerElement) { this.set(['lastUsedVariables', variable.name], newValue); } + _initVariableHistory() { + getHistoryEntries(25, ['request.variables.values']).then((entries) => { + entries.reverse(); + for (let entry of entries) { + this._addHistoryEntryToVariableHistory(entry); + } + }); + } + _onDataChanges(e) { for (let change of e.detail) { if (change.itemType === 'HistoryEntry' && change.action === 'add') { @@ -202,38 +214,30 @@ class RESTerVariablesInput extends RESTerVariablesMixin(PolymerElement) { _addHistoryEntryToVariableHistory(entry) { const values = entry.request.variables.values || {}; for (let name in values) { - if (Object.prototype.hasOwnProperty.call(values, name)) { - if ( - !Object.prototype.hasOwnProperty.call( - this.variableHistory, - name - ) - ) { - this.set(['variableHistory', name], [values[name]]); - } else { - const index = this.variableHistory[name].indexOf( - values[name] - ); - if (index > -1) { - this.splice(['variableHistory', name], index, 1); - } - - this.unshift(['variableHistory', name], values[name]); + if (!Object.hasOwn(values, name)) { + continue; + } + if (!Object.hasOwn(this.variableHistory, name)) { + this.set(['variableHistory', name], [values[name]]); + } else { + const index = this.variableHistory[name].indexOf(values[name]); + if (index > -1) { + this.splice(['variableHistory', name], index, 1); } - // Update the history property in the variable item, which is used - // in the dom-repeat element. - if (this.variables) { - const index = this.variables.findIndex( - (v) => v.name === name + this.unshift(['variableHistory', name], values[name]); + } + + // Update the history property in the variable item, which is used + // in the dom-repeat element. + if (this.variables) { + const index = this.variables.findIndex((v) => v.name === name); + if (index > -1) { + this.set(['variables', index, 'history'], []); + this.set( + ['variables', index, 'history'], + this.variableHistory[name] ); - if (index > -1) { - this.set(['variables', index, 'history'], []); - this.set( - ['variables', index, 'history'], - this.variableHistory[name] - ); - } } } } diff --git a/src/site/elements/data/rester-data-navigation.js b/src/site/elements/data/rester-data-navigation.js index d44b3afa..cade007b 100644 --- a/src/site/elements/data/rester-data-navigation.js +++ b/src/site/elements/data/rester-data-navigation.js @@ -32,10 +32,9 @@ class RESTerDataNavigation extends PolymerElement { super.connectedCallback(); if (this._nav.itemsCreated) { this._invalidateItems(); - } else { - this._nav.addEventListener('itemsCreated', this._invalidateItems); } + this._nav.addEventListener('itemsCreated', this._invalidateItems); this._nav.addEventListener('itemsChanged', this._updateItems); } diff --git a/src/site/elements/data/scripts/browser-request.js b/src/site/elements/data/scripts/browser-request.js index 954b73b4..cc93ebc6 100644 --- a/src/site/elements/data/scripts/browser-request.js +++ b/src/site/elements/data/scripts/browser-request.js @@ -1,32 +1,22 @@ -function ensureIncognitoAccess() { - return new Promise((resolve, reject) => { - chrome.extension.isAllowedIncognitoAccess((isAllowed) => { - if (isAllowed) { - resolve(); - } else { - reject( - "RESTer doesn't have access to incognito windows. Please allow access on the browser extension page and try again." - ); - } - }); - }); +async function ensureIncognitoAccess() { + const isAllowed = await chrome.extension.isAllowedIncognitoAccess(); + if (isAllowed) { + throw new Error("RESTer doesn't have access to incognito windows. Please allow access on the browser extension page and try again."); + } } -function getCookieStoreId(tab) { - return new Promise((resolve, reject) => { - if (tab.cookieStoreId) { - resolve(tab.cookieStoreId); - } else { - chrome.cookies.getAllCookieStores((stores) => { - const store = stores.find((s) => s.tabIds.includes(tab.id)); - if (store) { - resolve(store.id); - } else { - reject(); - } - }); - } - }); +async function getCookieStoreId(tab) { + if (tab.cookieStoreId) { + return tab.cookieStoreId; + } + + const stores = await chrome.cookies.getAllCookieStores(); + const store = stores.find((s) => s.tabIds.includes(tab.id)); + if (store) { + return store.id; + } else { + throw new Error('Didn\'t find any cookie store for tab ' + tab.id); + } } export function getMatchPatterns(url) { @@ -52,7 +42,7 @@ export function getMatchPatterns(url) { } function sendRequest(request) { - return new Promise(function (resolve, reject) { + return new Promise(function(resolve, reject) { const urlMatchPatterns = getMatchPatterns(request.targetUrl); let thisWindowId, thisTab, diff --git a/src/site/elements/data/scripts/encode.js b/src/site/elements/data/scripts/encode.js index ee541b6c..d24baf60 100644 --- a/src/site/elements/data/scripts/encode.js +++ b/src/site/elements/data/scripts/encode.js @@ -63,3 +63,29 @@ export function truncate(str, maxLength, ellipsis = '…') { return str; } } + +export function generateFormData(body, tempVariables) { + const rawData = decodeQueryString(body); + const variableValues = tempVariables.values; + const formData = new FormData(); + + for (let key in rawData) { + if (Object.prototype.hasOwnProperty.call(rawData, key)) { + const values = Array.isArray(rawData[key]) + ? rawData[key] + : [rawData[key]]; + for (let value of values) { + const fileMatch = /^\[(\$file\.[^}]*)\]$/gi.exec(value); + + if (fileMatch) { + const file = variableValues[fileMatch[1]]; + formData.append(key, file, file.name); + } else { + formData.append(key, value); + } + } + } + } + + return formData; +} diff --git a/src/site/elements/data/scripts/navigation.js b/src/site/elements/data/scripts/navigation.js index 738ca40d..fea10650 100644 --- a/src/site/elements/data/scripts/navigation.js +++ b/src/site/elements/data/scripts/navigation.js @@ -154,14 +154,7 @@ function findNextRequestId(requestId, requestItems) { export default class Navigation extends CustomEventTarget { constructor() { super(); - this.items = []; - this.itemsCreated = false; - - this._requestNavItemsOffset = 0; - this._requestNavItemsCount = 0; - this._environmentNavItemIndex = 0; - this._historyNavItemsOffset = 0; - this._historyNavItemsCount = 0; + this._create = this._create.bind(this); this._updateNavigationBasedOnDataChanges = this._updateNavigationBasedOnDataChanges.bind(this); this._updateNavigationBasedOnSettingsChanges = @@ -176,9 +169,18 @@ export default class Navigation extends CustomEventTarget { 'settingsChange', this._updateNavigationBasedOnSettingsChanges ); + resterEvents.addEventListener('connectionReset', this._create); } async _create() { + this.items = []; + this.itemsCreated = false; + this._requestNavItemsOffset = 0; + this._requestNavItemsCount = 0; + this._environmentNavItemIndex = 0; + this._historyNavItemsOffset = 0; + this._historyNavItemsCount = 0; + const [requests, historyEntries, activeEnvironment] = await Promise.all( [ getRequests(requestFields), diff --git a/src/site/elements/data/scripts/request.js b/src/site/elements/data/scripts/request.js index 7b2aeaa6..aa307895 100644 --- a/src/site/elements/data/scripts/request.js +++ b/src/site/elements/data/scripts/request.js @@ -1,7 +1,21 @@ -import { decodeQueryString } from './encode.js'; -import { mergeCookies, parseStatusLine } from '../../../../shared/util.js'; +// TODO: Missing features with declarativeNetRequest: +// - Read the original resonse statusCode, statusText and headers. +// - Read original timing-allow-origin response header. +// - Append response header 'access-control-expose-headers' for all responses +// headers. Can't use '*' here because it's only a wildcard in requests +// without credentials. +// - declarativeNetRequest doesn't seem to work with optional_permissions + +import { generateFormData } from './encode.js'; +import { + mergeCookies, + parseStatusLine, + getFilenameFromContentDispositionHeader, +} from '../../../../shared/util.js'; import { downloadBlob } from '../../../../shared/download-blob.js'; +const API = isFirefox() ? 'webRequest' : 'declarativeNetRequest'; + const headerPrefix = `x-rester-49ba6c3c4d3e4c069630b903fb211cf8-`; const headerCommandPrefix = `x-rester-command-49ba6c3c4d3e4c069630b903fb211cf8-`; const requiredDefaultHeaders = { @@ -16,48 +30,48 @@ const requestIds = new Map(); // Maps from RESTer requestId to original response headers. const originalResponses = new Map(); -function requestWebRequestPermissions() { - const requiredPermissions = { - permissions: ['webRequest', 'webRequestBlocking'], - }; - - return new Promise((resolve, reject) => { - chrome.permissions.request(requiredPermissions, (granted) => { - if (granted) { - resolve(); - } else { - reject(chrome.runtime.lastError); - } - }); - }); +const defaultRequestHeaders = [ + 'accept', + 'accept-encoding', + 'accept-language', + 'cache-control', + 'cookie', + 'origin', + 'pragma', + 'user-agent', +]; + +function isFirefox() { + const manifest = chrome.runtime.getManifest(); + return manifest.browser_specific_settings?.gecko != null; } -function getCurrentTabId() { - return new Promise((resolve) => { - chrome.tabs.getCurrent((tab) => { - resolve(tab.id); - }); - }); +async function getCurrentTabId() { + const tab = await chrome.tabs.getCurrent(); + return tab.id; } let headerInterceptorPromise; export function ensureHeaderInterceptor() { if (!headerInterceptorPromise) { headerInterceptorPromise = (async function () { - try { - await requestWebRequestPermissions(); - setupHeaderInterceptor(await getCurrentTabId()); - return true; - } catch (e) { + const requiredPermissions = { + permissions: ['webRequest', 'webRequestBlocking'], + }; + const granted = await chrome.permissions.request( + requiredPermissions + ); + if (!granted) { console.warn( 'RESTer could not install the header interceptor. ' + 'Certain features like setting cookies or the ' + - '"Clean Request" mode will not work as expected. ' + - 'Reason: ' + - e.message + '"Clean Request" mode will not work as expected.' ); return false; } + + setupHeaderInterceptor(await getCurrentTabId()); + return true; })(); } @@ -234,67 +248,77 @@ function setupHeaderInterceptor(currentTabId) { ); } -function generateFormData(body, tempVariables) { - const rawData = decodeQueryString(body); - const variableValues = tempVariables.values; - const formData = new FormData(); - - for (let key in rawData) { - if (Object.prototype.hasOwnProperty.call(rawData, key)) { - const values = Array.isArray(rawData[key]) - ? rawData[key] - : [rawData[key]]; - for (let value of values) { - const fileMatch = /^\[(\$file\.[^}]*)\]$/gi.exec(value); - - if (fileMatch) { - const file = variableValues[fileMatch[1]]; - formData.append(key, file, file.name); - } else { - formData.append(key, value); - } - } +function setHeadersViaHeaderInterceptor(headers, stripDefaultHeaders) { + const result = new Headers(); + const requestId = String(Math.random()); + result.append(headerCommandPrefix + 'requestid', requestId); + if (stripDefaultHeaders) { + result.append(headerCommandPrefix + 'stripdefaultheaders', 'true'); + } + + for (const header of headers) { + if (header && header.name && header.value) { + result.append( + headerPrefix + header.name, + JSON.stringify({ name: header.name, value: header.value }) + ); } } - return formData; + return { headers: result, requestId }; } -export function getFilenameFromContentDispositionHeader(disposition) { - const utf8FilenameRegex = /filename\*=UTF-8''([\w%\-.]+)(?:; ?|$)/i; - const asciiFilenameRegex = /filename=(["']?)(.*?[^\\])\1(?:; ?|$)/i; - - let fileName = null; - if (utf8FilenameRegex.test(disposition)) { - fileName = decodeURIComponent(utf8FilenameRegex.exec(disposition)[1]); - } else { - // Prevent ReDos attacks by anchoring the ascii regex to string start - // and slicing off everything before 'filename=' - const filenameStart = disposition.toLowerCase().indexOf('filename='); - if (filenameStart >= 0) { - const partialDisposition = disposition.slice(filenameStart); - const matches = asciiFilenameRegex.exec(partialDisposition); - if (matches !== null && matches[2]) { - fileName = matches[2]; - } +async function setHeadersViaDeclarativeNetRequest( + headers, + stripDefaultHeaders +) { + const tabId = await getCurrentTabId(); + const requestHeaders = []; + if (stripDefaultHeaders) { + for (const headerName of defaultRequestHeaders) { + requestHeaders.push({ + header: headerName, + operation: 'remove', + }); } } - - if (fileName !== null) { - // Sanitize filename for illegal characters - const illegalRe = /[/?<>\\:*|":]/g; - const controlRe = /[\x00-\x1f\x80-\x9f]/g; - const reservedRe = /^\.+/g; - const windowsReservedRe = - /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; - fileName = fileName - .replace(illegalRe, '') - .replace(controlRe, '') - .replace(reservedRe, '') - .replace(windowsReservedRe, ''); + for (const header of headers) { + if (header && header.name && header.value) { + requestHeaders.push({ + header: header.name, + operation: 'set', + value: header.value, + }); + } } - return fileName; + const responseHeaders = [ + { + header: 'timing-allow-origin', + operation: 'set', + value: '*', + }, + ]; + const addRules = [ + { + action: { + type: 'modifyHeaders', + requestHeaders: + requestHeaders.length > 0 ? requestHeaders : undefined, + responseHeaders, + }, + condition: { + resourceTypes: ['xmlhttprequest'], + tabIds: [tabId], + }, + id: tabId, + }, + ]; + const removeRuleIds = [tabId]; + await chrome.declarativeNetRequest.updateSessionRules({ + addRules, + removeRuleIds, + }); } /** @@ -319,9 +343,8 @@ export function getFilenameFromContentDispositionHeader(disposition) { * was successfully saved and returns the request response. */ export async function send(request) { - const headersIntercepted = await ensureHeaderInterceptor(); - - const requestId = String(Math.random()); + const headersIntercepted = + API === 'webRequest' && (await ensureHeaderInterceptor()); // Special handling for multipart requests. const contentTypeIndex = request.headers.findIndex( @@ -336,22 +359,23 @@ export async function send(request) { } // Create fetch request options. - const headers = new Headers(); - if (headersIntercepted) { - headers.append(headerCommandPrefix + 'requestid', requestId); - if (request.stripDefaultHeaders) { - headers.append(headerCommandPrefix + 'stripdefaultheaders', 'true'); - } - - for (const header of requestHeaders) { - if (header && header.name && header.value) { - headers.append( - headerPrefix + header.name, - JSON.stringify({ name: header.name, value: header.value }) - ); - } - } + let headers; + let requestId; + if (API === 'webRequest' && headersIntercepted) { + const result = setHeadersViaHeaderInterceptor( + requestHeaders, + request.stripDefaultHeaders + ); + headers = result.headers; + requestId = result.requestId; + } else if (API === 'declarativeNetRequest') { + await setHeadersViaDeclarativeNetRequest( + requestHeaders, + request.stripDefaultHeaders + ); + headers = new Headers(); } else { + headers = new Headers(); for (const header of requestHeaders) { if (header && header.name && header.value) { headers.append(header.name, header.value); @@ -384,7 +408,7 @@ export async function send(request) { const fetchResponse = await fetch(request.url, init); response.redirected = fetchResponse.redirected; - if (headersIntercepted) { + if (API === 'webRequest' && headersIntercepted) { const originalResponse = originalResponses.get(requestId); originalResponses.delete(requestId); response.status = originalResponse.status; diff --git a/src/site/elements/data/scripts/request.test.js b/src/site/elements/data/scripts/request.test.js deleted file mode 100644 index 3477f3cb..00000000 --- a/src/site/elements/data/scripts/request.test.js +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-env node, jest */ - -import { getFilenameFromContentDispositionHeader } from './request.js'; - -describe('getFilenameFromContentDispositionHeader', function () { - test('utf8', function () { - expect( - getFilenameFromContentDispositionHeader( - "attachment;filename*=UTF-8''%6A%C5%AF%C5%AF%C5%AF%C5%BE%C4%9B%2E%74%78%74" - ) - ).toEqual('jůůůžě.txt'); - }); - - test('ascii', function () { - expect( - getFilenameFromContentDispositionHeader( - 'attachment; filename="cool.html"' - ) - ).toEqual('cool.html'); - }); - - test('reverved characters', function () { - expect( - getFilenameFromContentDispositionHeader( - 'attachment; filename="../c|o?ol.html"' - ) - ).toEqual('cool.html'); - }); -}); diff --git a/src/site/elements/data/scripts/rester.js b/src/site/elements/data/scripts/rester.js index 5c7a83b7..4f44af94 100644 --- a/src/site/elements/data/scripts/rester.js +++ b/src/site/elements/data/scripts/rester.js @@ -2,7 +2,6 @@ import CustomEventTarget from '../../../../shared/custom-event-target.js'; export const e = new CustomEventTarget(); -const port = chrome.runtime.connect({ name: 'api' }); const requests = {}; const settingsKeys = [ 'activeEnvironment', @@ -20,29 +19,57 @@ const settingsKeys = [ ]; const cachedSettings = {}; -port.onMessage.addListener((message) => { - if (message.action === 'apiresponse') { - if (message.error) { - requests[message.id].reject( - message.error && JSON.parse(message.error) - ); - } else { - requests[message.id].resolve( - message.result && JSON.parse(message.result) - ); - } - - requests[message.id] = undefined; - } else if (message.action.startsWith('event.')) { - const eventName = message.action.split('.')[1]; - const detail = message.detail && JSON.parse(message.detail); - - if (eventName === 'settingsChange' && cachedSettings) { - Object.assign(cachedSettings, detail); +let port; +let portIsConnected = false; +function reconnectPort() { + if (portIsConnected) { + throw new Error('Reconnect when port is already connected.'); + } + const isReconnect = port != null; + + port = chrome.runtime.connect({ name: 'api' }); + port.onMessage.addListener((message) => { + if (message.action === 'apiresponse') { + if (message.error) { + requests[message.id].reject( + message.error && JSON.parse(message.error) + ); + } else { + requests[message.id].resolve( + message.result && JSON.parse(message.result) + ); + } + + requests[message.id] = undefined; + } else if (message.action.startsWith('event.')) { + const eventName = message.action.split('.')[1]; + const detail = message.detail && JSON.parse(message.detail); + + if (eventName === 'settingsChange' && cachedSettings) { + Object.assign(cachedSettings, detail); + } + + e.dispatchEvent(new CustomEvent(eventName, { detail })); } + }); + port.onDisconnect.addListener(() => { + portIsConnected = false; + }); + portIsConnected = true; - e.dispatchEvent(new CustomEvent(eventName, { detail })); + if (isReconnect) { + e.dispatchEvent(new CustomEvent('connectionReset')); } +} +reconnectPort(); + +e.addEventListener('connectionReset', () => { + sendApiRequest('settings.get').then((settings) => { + Object.assign(cachedSettings, settings); + e.dispatchEvent( + new CustomEvent('settingsChange', { detail: settings }) + ); + }); }); function sendApiRequest(action, args, fields) { @@ -51,6 +78,9 @@ function sendApiRequest(action, args, fields) { requests[id] = { resolve, reject }; + if (!portIsConnected) { + reconnectPort(); + } port.postMessage({ id, action: 'api.' + action, diff --git a/src/site/elements/data/scripts/variables-provider-env.js b/src/site/elements/data/scripts/variables-provider-env.js index 634f510e..b1361880 100644 --- a/src/site/elements/data/scripts/variables-provider-env.js +++ b/src/site/elements/data/scripts/variables-provider-env.js @@ -36,5 +36,6 @@ function updateValues() { settingsLoaded.then(updateValues); resterEvents.addEventListener('dataChange', updateValues); resterEvents.addEventListener('settingsChange', updateValues); +resterEvents.addEventListener('connectionReset', updateValues); export default provider; diff --git a/tools/lib/copy-manifest-and-icons.js b/tools/lib/copy-manifest-and-icons.js new file mode 100644 index 00000000..601af3d3 --- /dev/null +++ b/tools/lib/copy-manifest-and-icons.js @@ -0,0 +1,57 @@ +'use strict'; + +const { readFile, mkdir, writeFile } = require('fs/promises'); +const path = require('path'); + +function replaceDevWithProdIcons(manifest) { + const parsed = JSON.parse(manifest); + for (const key of Object.keys(parsed.icons)) { + parsed.icons[key] = parsed.icons[key].replace('-dev', ''); + } + for (const key of Object.keys(parsed.action.default_icon)) { + parsed.action.default_icon[key] = parsed.action.default_icon[ + key + ].replace('-dev', ''); + } + if (parsed.action.theme_icons) { + for (const icon of parsed.action.theme_icons) { + icon.dark = icon.dark.replace('-dev', ''); + icon.light = icon.light.replace('-dev', ''); + } + } + return JSON.stringify(parsed, null, 4); +} + +function getReferencedIcons(manifest) { + const parsed = JSON.parse(manifest); + const icons = [ + ...Object.values(parsed.icons), + ...Object.values(parsed.action.default_icon), + ]; + if (parsed.action.theme_icons) { + for (const icon of parsed.action.theme_icons) { + icons.push(icon.dark); + icons.push(icon.light); + } + } + return icons; +} + +async function copyManifestAndIcons({ browser, srcDir, destDir, mode }) { + const manifestPath = path.join(srcDir, `manifest-${browser}.json`); + let manifest = await readFile(manifestPath, 'utf8'); + if (mode === 'production') { + manifest = replaceDevWithProdIcons(manifest); + } + const icons = getReferencedIcons(manifest); + + await mkdir(destDir, { recursive: true }); + await writeFile(path.join(destDir, 'manifest.json'), manifest, 'utf8'); + for (const icon of icons) { + const destFile = path.join(destDir, icon); + await mkdir(path.dirname(destFile), { recursive: true }); + await writeFile(destFile, await readFile(path.join(srcDir, icon))); + } +} + +module.exports = copyManifestAndIcons; diff --git a/tools/lib/create-package.js b/tools/lib/create-package.js index 5ad72a91..7319e50e 100644 --- a/tools/lib/create-package.js +++ b/tools/lib/create-package.js @@ -8,88 +8,22 @@ const archiver = require('archiver'); const packageJson = require('../../package.json'); -const additionalManifestEntries = { - firefox: { - applications: { - gecko: { - id: 'rester@kuehle.me', - strict_min_version: '63.0', - }, - }, - icons: { - 48: 'images/icon48.png', - 96: 'images/icon96.png', - }, - browser_action: { - default_icon: { - 16: 'images/icon16.png', - 24: 'images/icon24.png', - 32: 'images/icon32.png', - }, - theme_icons: [ - { - dark: 'images/icon16.png', - light: 'images/icon-light16.png', - size: 16, - }, - { - dark: 'images/icon24.png', - light: 'images/icon-light24.png', - size: 24, - }, - { - dark: 'images/icon32.png', - light: 'images/icon-light32.png', - size: 32, - }, - ], - }, - }, - chrome: { - minimum_chrome_version: '67', - icons: { - 48: 'images/icon48.png', - 128: 'images/icon128.png', - }, - browser_action: { - default_icon: { - 16: 'images/icon16.png', - 24: 'images/icon24.png', - 32: 'images/icon32.png', - }, - }, - }, -}; - -const usedImages = { - firefox: [ - 'images/icon{16,24,32,48,96}.png', - 'images/icon-light{16,24,32,48,96}.png', - ], - chrome: ['images/icon{16,24,32,48,128}.png'], -}; - -function enhanceManifestJson(manifestJson, browser) { +function validateManifest(manifestJson) { const manifest = JSON.parse(manifestJson); - // Add additional keys. - Object.assign(manifest, additionalManifestEntries[browser]); - - // Validate version if (manifest.version !== packageJson.version) { throw new Error( `Version in manifest (${manifest.version}) does not match validated version (${packageJson.version}).` ); } - - return JSON.stringify(manifest, null, 4); } -async function createPackage({ browser, srcDir, destFile }) { +async function createPackage({ srcDir, destFile }) { const manifestJson = await readFile( path.join(srcDir, 'manifest.json'), 'utf8' ); + validateManifest(manifestJson); await mkdir(path.dirname(destFile), { recursive: true }); return await new Promise((resolve, reject) => { @@ -97,32 +31,14 @@ async function createPackage({ browser, srcDir, destFile }) { const archive = archiver('zip', { zlib: { level: 9 }, }); - output.on('close', function () { resolve(); }); - archive.on('error', function (err) { reject(err); }); - archive.pipe(output); - - archive.glob('**', { - cwd: srcDir, - ignore: ['images/**', 'manifest.json'], - }); - - for (const images of usedImages[browser]) { - archive.glob(images, { - cwd: srcDir, - }); - } - - archive.append(enhanceManifestJson(manifestJson, browser), { - name: 'manifest.json', - }); - + archive.glob('**', { cwd: srcDir, follow: true }); archive.finalize(); }); } diff --git a/tools/lib/create-web-driver.js b/tools/lib/create-web-driver.js index 5602b259..2fa322e5 100644 --- a/tools/lib/create-web-driver.js +++ b/tools/lib/create-web-driver.js @@ -28,7 +28,7 @@ function ensureGeckoDriverInPath() { } async function createResterExtensionXpi() { - const srcDir = path.resolve(rootDir, 'build'); + const srcDir = path.resolve(rootDir, 'build/firefox'); const xpiPath = path.resolve(rootDir, 'package/firefox-selenium.xpi'); try { diff --git a/tools/scripts/copy-manifest-and-icons.mjs b/tools/scripts/copy-manifest-and-icons.mjs new file mode 100644 index 00000000..bc403a28 --- /dev/null +++ b/tools/scripts/copy-manifest-and-icons.mjs @@ -0,0 +1,56 @@ +/* eslint-disable no-console */ + +'use strict'; + +import { fileURLToPath } from 'url'; +import * as path from 'path'; +import { symlink, mkdir, lstat } from 'fs/promises'; + +import copyManifestAndIcons from '../lib/copy-manifest-and-icons.js'; + +async function linkDir(destBaseDir, dir) { + const chromeDir = path.join(destBaseDir, 'chrome', dir); + let exists; + try { + const stat = await lstat(chromeDir); + exists = stat.isSymbolicLink(); + } catch (e) { + exists = false; + } + + if (!exists) { + await mkdir(path.join(destBaseDir, 'firefox', dir), { + recursive: true, + }); + await symlink( + path.join(destBaseDir, 'firefox', dir), + chromeDir, + 'junction' + ); + } +} + +async function main() { + const mode = process.argv[2]; + + const srcDir = fileURLToPath(new URL('../../src', import.meta.url)); + const destBaseDir = fileURLToPath(new URL(`../../build`, import.meta.url)); + + await copyManifestAndIcons({ + browser: 'firefox', + srcDir, + destDir: path.join(destBaseDir, 'firefox'), + mode, + }); + await copyManifestAndIcons({ + browser: 'chrome', + srcDir, + destDir: path.join(destBaseDir, 'chrome'), + mode, + }); + + await linkDir(destBaseDir, 'background'); + await linkDir(destBaseDir, 'site'); +} + +main().catch((err) => console.error(err.stack)); diff --git a/tools/scripts/lint-firefox-addon.mjs b/tools/scripts/lint-firefox-addon.mjs index bcdab55d..bfb48d0f 100644 --- a/tools/scripts/lint-firefox-addon.mjs +++ b/tools/scripts/lint-firefox-addon.mjs @@ -11,6 +11,7 @@ import chalk from 'chalk'; import logSymbols from 'log-symbols'; const ignoreFileName = '.addonslinterignore'; +const noFileToken = ''; /** * Lints a Firefox addon. @@ -47,8 +48,7 @@ async function lintFirefoxAddon(options) { result.count = 0; for (let list of lists) { result[list] = result[list].filter((message) => { - const file = message.file ? resolvePath(message.file) : ''; - + const file = message.file ? resolvePath(message.file) : noFileToken; const ignoreEntry = ignoreList.find( (ignore) => ignore.file === file && ignore.code === message.code ); @@ -100,6 +100,10 @@ function getIgnoreList() { // let [file, code] = line.split(' '); + if (file !== noFileToken) { + file = resolvePath(file); + } + // Some codes sentences instead of short, uppercase names. In // this case the code should start with "_" in the ignore file // and all spaces should be replaced by "_". @@ -107,10 +111,7 @@ function getIgnoreList() { code = code.substr(1).replace(/_/g, ' '); } - ignore.push({ - file: resolvePath(file), - code, - }); + ignore.push({ file, code }); }); lineReader.on('close', () => { diff --git a/tools/scripts/package-addon.mjs b/tools/scripts/package-addon.mjs index 420dcd93..350a2221 100644 --- a/tools/scripts/package-addon.mjs +++ b/tools/scripts/package-addon.mjs @@ -13,7 +13,9 @@ async function main() { const packageJson = JSON.parse( await readFile(new URL('../../package.json', import.meta.url)) ); - const srcDir = fileURLToPath(new URL('../../build', import.meta.url)); + const srcDir = fileURLToPath( + new URL(`../../build/${browser}`, import.meta.url) + ); const destFile = fileURLToPath( new URL( `../../package/${browser}-${packageJson.version}.zip`, @@ -21,7 +23,7 @@ async function main() { ) ); - await createPackage({ browser, srcDir, destFile }); + await createPackage({ srcDir, destFile }); console.log(`Created file ${destFile}.`); } diff --git a/webpack.config.js b/webpack.config.js index 031bd148..c45750b5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,7 +16,7 @@ module.exports = [ entry: './index.js', output: { filename: 'bundle.js', - path: path.resolve(__dirname, 'build/background'), + path: path.resolve(__dirname, 'build/firefox/background'), }, }, { @@ -25,7 +25,7 @@ module.exports = [ entry: './index.js', output: { filename: 'scripts/bundle.js', - path: path.resolve(__dirname, 'build/site'), + path: path.resolve(__dirname, 'build/firefox/site'), }, resolve: { alias: { diff --git a/yarn.lock b/yarn.lock index 26b126a5..c79c37be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2358,14 +2358,6 @@ agent-base@6: dependencies: debug "4" -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -2805,11 +2797,6 @@ clean-css@^5.2.2: dependencies: source-map "~0.6.0" -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -2988,25 +2975,6 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== -cpx2@4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/cpx2/-/cpx2-4.2.0.tgz#d65c52fc1e804f6567f57868cd1e88aaab367316" - integrity sha512-Ik81d7J849x0dGpR/8TBLXc1MwkFuv29kkstgLau8IOQwptrEENsXefC4o+tnkTjiFnXbsaz08/6YSZdJER+nQ== - dependencies: - debounce "^1.2.0" - debug "^4.1.1" - duplexer "^0.1.1" - fs-extra "^10.0.0" - glob-gitignore "^1.0.14" - glob2base "0.0.12" - ignore "^5.1.8" - minimatch "^3.0.4" - p-map "^4.0.0" - resolve "^1.12.0" - safe-buffer "^5.2.0" - shell-quote "^1.7.1" - subarg "^1.0.0" - crc-32@^1.2.0: version "1.2.2" resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" @@ -3087,11 +3055,6 @@ date-fns@^2.29.1: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.2.tgz#0d4b3d0f3dff0f920820a070920f0d9662c51931" integrity sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA== -debounce@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5" - integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== - debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -3249,11 +3212,6 @@ dot-case@^3.0.4: no-case "^3.0.4" tslib "^2.0.3" -duplexer@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - electron-to-chromium@^1.4.251: version "1.4.254" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.254.tgz#c6203583890abf88dfc0be046cd72d3b48f8beb6" @@ -3680,11 +3638,6 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -find-index@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" - integrity sha512-uJ5vWrfBKMcE6y2Z8834dwEZj9mNGxYa3t3I53OwFeuZ8D9oc2E5zcsrkuhX6h4iYrjhiv0T3szQmxlAV9uxDg== - find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -3738,15 +3691,6 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" - integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -3807,18 +3751,6 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== -glob-gitignore@^1.0.14: - version "1.0.14" - resolved "https://registry.yarnpkg.com/glob-gitignore/-/glob-gitignore-1.0.14.tgz#8b708cb029e73bd388d22f7f935213aa93a1e7c2" - integrity sha512-YuAEPqL58bOQDqDF2kMv009rIjSAtPs+WPzyGbwRWK+wD0UWQVRoP34Pz6yJ6ivco65C9tZnaIt0I3JCuQ8NZQ== - dependencies: - glob "^7.1.3" - ignore "^5.0.5" - lodash.difference "^4.5.0" - lodash.union "^4.6.0" - make-array "^1.0.5" - util.inherits "^1.0.3" - glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -3838,13 +3770,6 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob2base@0.0.12: - version "0.0.12" - resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56" - integrity sha512-ZyqlgowMbfj2NPjxaZZ/EtsXlOch28FRXgMd64vqZWk1bT9+wvSRLYD1om9M7QfQru51zJPAT17qXm4/zd+9QA== - dependencies: - find-index "^0.1.1" - glob@8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" @@ -3918,7 +3843,7 @@ got@11.8.5: p-cancelable "^2.0.0" responselike "^2.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== @@ -4058,7 +3983,7 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.0.5, ignore@^5.1.8, ignore@^5.2.0: +ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== @@ -4096,11 +4021,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -4795,15 +4715,6 @@ json5@^2.1.2, json5@^2.2.2: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - jszip@^3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.0.tgz#faf3db2b4b8515425e34effcdbb086750a346061" @@ -4986,11 +4897,6 @@ lru-cache@^7.14.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== -make-array@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/make-array/-/make-array-1.0.5.tgz#326a7635c756a9f61ce0b2a6fdd5cc3460419bcb" - integrity sha512-sgK2SAzxT19rWU+qxKUcn6PAh/swiIiz2F8C2cZjLc1z4iwYIfdoihqFIDQ8BDzAGtWPYJ6Sr13K1j/DXynDLA== - make-dir@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -5087,11 +4993,6 @@ minimatch@^7.4.1: dependencies: brace-expansion "^2.0.1" -minimist@^1.1.0: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== - minipass@^3.0.0: version "3.3.4" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.4.tgz#ca99f95dd77c43c7a76bf51e6d200025eee0ffae" @@ -5284,13 +5185,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -5726,7 +5620,7 @@ resolve.exports@^2.0.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.0.tgz#c1a0028c2d166ec2fbf7d0644584927e76e7400e" integrity sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg== -resolve@^1.12.0, resolve@^1.14.2, resolve@^1.20.0: +resolve@^1.14.2, resolve@^1.20.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== @@ -5775,7 +5669,7 @@ rxjs@^7.0.0: dependencies: tslib "^2.1.0" -safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -5886,7 +5780,7 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.7.1, shell-quote@^1.7.3: +shell-quote@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== @@ -6047,13 +5941,6 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -subarg@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" - integrity sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg== - dependencies: - minimist "^1.1.0" - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -6267,11 +6154,6 @@ universalify@^0.1.2: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - upath@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" @@ -6297,11 +6179,6 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -util.inherits@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/util.inherits/-/util.inherits-1.0.3.tgz#a9c626a0d06d34829d47ba56cab1278d745f9ce6" - integrity sha512-gMirHcfcq5D87nXDwbZqf5vl65S0mpMZBsHXJsXOO3Hc3G+JoQLwgaJa1h+PL7h3WhocnuLqoe8CuvMlztkyCA== - utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"