зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1454378 - cache and inline things, avoid duplicate attribute or property requests, to make blocklist faster, r=florian
MozReview-Commit-ID: BwBhZr6sqx2 --HG-- extra : rebase_source : e5cf8d9b51118730701fe7b09befc006413b33aa
This commit is contained in:
Родитель
d0c0e7fcc2
Коммит
9f327f0d76
|
@ -181,6 +181,14 @@ XPCOMUtils.defineLazyGetter(this, "gApp", function() {
|
||||||
return appinfo;
|
return appinfo;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyGetter(this, "gAppID", function() {
|
||||||
|
return gApp.ID;
|
||||||
|
});
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyGetter(this, "gAppVersion", function() {
|
||||||
|
return gApp.version;
|
||||||
|
});
|
||||||
|
|
||||||
XPCOMUtils.defineLazyGetter(this, "gABI", function() {
|
XPCOMUtils.defineLazyGetter(this, "gABI", function() {
|
||||||
let abi = null;
|
let abi = null;
|
||||||
try {
|
try {
|
||||||
|
@ -248,14 +256,16 @@ function restartApp() {
|
||||||
* Whether the entry matches the current OS.
|
* Whether the entry matches the current OS.
|
||||||
*/
|
*/
|
||||||
function matchesOSABI(blocklistElement) {
|
function matchesOSABI(blocklistElement) {
|
||||||
if (blocklistElement.hasAttribute("os")) {
|
let os = blocklistElement.getAttribute("os");
|
||||||
var choices = blocklistElement.getAttribute("os").split(",");
|
if (os) {
|
||||||
|
let choices = os.split(",");
|
||||||
if (choices.length > 0 && !choices.includes(gApp.OS))
|
if (choices.length > 0 && !choices.includes(gApp.OS))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blocklistElement.hasAttribute("xpcomabi")) {
|
let xpcomabi = blocklistElement.getAttribute("xpcomabi");
|
||||||
choices = blocklistElement.getAttribute("xpcomabi").split(",");
|
if (xpcomabi) {
|
||||||
|
let choices = xpcomabi.split(",");
|
||||||
if (choices.length > 0 && !choices.includes(gApp.XPCOMABI))
|
if (choices.length > 0 && !choices.includes(gApp.XPCOMABI))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -279,22 +289,6 @@ function getDistributionPrefValue(aPrefName) {
|
||||||
return Services.prefs.getDefaultBranch(null).getCharPref(aPrefName, "default");
|
return Services.prefs.getDefaultBranch(null).getCharPref(aPrefName, "default");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a string representation of a regular expression. Needed because we
|
|
||||||
* use the /pattern/flags form (because it's detectable), which is only
|
|
||||||
* supported as a literal in JS.
|
|
||||||
*
|
|
||||||
* @param {string} aStr
|
|
||||||
* String representation of regexp
|
|
||||||
* @return {RegExp} instance
|
|
||||||
*/
|
|
||||||
function parseRegExp(aStr) {
|
|
||||||
let lastSlash = aStr.lastIndexOf("/");
|
|
||||||
let pattern = aStr.slice(1, lastSlash);
|
|
||||||
let flags = aStr.slice(lastSlash + 1);
|
|
||||||
return new RegExp(pattern, flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the Blocklist. The Blocklist is a representation of the contents of
|
* Manages the Blocklist. The Blocklist is a representation of the contents of
|
||||||
* blocklist.xml and allows us to remotely disable / re-enable blocklisted
|
* blocklist.xml and allows us to remotely disable / re-enable blocklisted
|
||||||
|
@ -441,11 +435,11 @@ var Blocklist = {
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
|
// Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
|
||||||
if (!appVersion && !gApp.version)
|
if (!appVersion && !gAppVersion)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (!appVersion)
|
if (!appVersion)
|
||||||
appVersion = gApp.version;
|
appVersion = gAppVersion;
|
||||||
if (!toolkitVersion)
|
if (!toolkitVersion)
|
||||||
toolkitVersion = gApp.platformVersion;
|
toolkitVersion = gApp.platformVersion;
|
||||||
|
|
||||||
|
@ -536,7 +530,7 @@ var Blocklist = {
|
||||||
// (For every non-null property in entry, the same key must exist in
|
// (For every non-null property in entry, the same key must exist in
|
||||||
// params and value must be the same)
|
// params and value must be the same)
|
||||||
function checkEntry(entry, params) {
|
function checkEntry(entry, params) {
|
||||||
for (let [key, value] of entry) {
|
for (let [key, value] of Object.entries(entry)) {
|
||||||
if (value === null || value === undefined)
|
if (value === null || value === undefined)
|
||||||
continue;
|
continue;
|
||||||
if (params[key]) {
|
if (params[key]) {
|
||||||
|
@ -612,27 +606,33 @@ var Blocklist = {
|
||||||
if (pingCountTotal < 1)
|
if (pingCountTotal < 1)
|
||||||
pingCountTotal = 1;
|
pingCountTotal = 1;
|
||||||
|
|
||||||
dsURI = dsURI.replace(/%APP_ID%/g, gApp.ID);
|
let replacements = {
|
||||||
// Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
|
APP_ID: gAppID,
|
||||||
if (gApp.version)
|
PRODUCT: gApp.name,
|
||||||
dsURI = dsURI.replace(/%APP_VERSION%/g, gApp.version);
|
BUILD_ID: gApp.appBuildID,
|
||||||
dsURI = dsURI.replace(/%PRODUCT%/g, gApp.name);
|
BUILD_TARGET: gApp.OS + "_" + gABI,
|
||||||
// Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
|
OS_VERSION: gOSVersion,
|
||||||
if (gApp.version)
|
LOCALE: getLocale(),
|
||||||
dsURI = dsURI.replace(/%VERSION%/g, gApp.version);
|
CHANNEL: UpdateUtils.UpdateChannel,
|
||||||
dsURI = dsURI.replace(/%BUILD_ID%/g, gApp.appBuildID);
|
PLATFORM_VERSION: gApp.platformVersion,
|
||||||
dsURI = dsURI.replace(/%BUILD_TARGET%/g, gApp.OS + "_" + gABI);
|
DISTRIBUTION: getDistributionPrefValue(PREF_APP_DISTRIBUTION),
|
||||||
dsURI = dsURI.replace(/%OS_VERSION%/g, gOSVersion);
|
DISTRIBUTION_VERSION: getDistributionPrefValue(PREF_APP_DISTRIBUTION_VERSION),
|
||||||
dsURI = dsURI.replace(/%LOCALE%/g, getLocale());
|
PING_COUNT: pingCountVersion,
|
||||||
dsURI = dsURI.replace(/%CHANNEL%/g, UpdateUtils.UpdateChannel);
|
TOTAL_PING_COUNT: pingCountTotal,
|
||||||
dsURI = dsURI.replace(/%PLATFORM_VERSION%/g, gApp.platformVersion);
|
DAYS_SINCE_LAST_PING: daysSinceLastPing,
|
||||||
dsURI = dsURI.replace(/%DISTRIBUTION%/g,
|
};
|
||||||
getDistributionPrefValue(PREF_APP_DISTRIBUTION));
|
dsURI = dsURI.replace(/%([A-Z_]+)%/g, function(fullMatch, name) {
|
||||||
dsURI = dsURI.replace(/%DISTRIBUTION_VERSION%/g,
|
// Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
|
||||||
getDistributionPrefValue(PREF_APP_DISTRIBUTION_VERSION));
|
if (gAppVersion && (name == "APP_VERSION" || name == "VERSION")) {
|
||||||
dsURI = dsURI.replace(/%PING_COUNT%/g, pingCountVersion);
|
return gAppVersion;
|
||||||
dsURI = dsURI.replace(/%TOTAL_PING_COUNT%/g, pingCountTotal);
|
}
|
||||||
dsURI = dsURI.replace(/%DAYS_SINCE_LAST_PING%/g, daysSinceLastPing);
|
// Some items, like DAYS_SINCE_LAST_PING, can be undefined, so we can't just
|
||||||
|
// `return replacements[name] || fullMatch` or something like that.
|
||||||
|
if (!replacements.hasOwnProperty(name)) {
|
||||||
|
return fullMatch;
|
||||||
|
}
|
||||||
|
return replacements[name];
|
||||||
|
});
|
||||||
dsURI = dsURI.replace(/\+/g, "%2B");
|
dsURI = dsURI.replace(/\+/g, "%2B");
|
||||||
|
|
||||||
// Under normal operations it will take around 5,883,516 years before the
|
// Under normal operations it will take around 5,883,516 years before the
|
||||||
|
@ -922,33 +922,38 @@ var Blocklist = {
|
||||||
versions: [],
|
versions: [],
|
||||||
prefs: [],
|
prefs: [],
|
||||||
blockID: null,
|
blockID: null,
|
||||||
attributes: new Map()
|
attributes: {},
|
||||||
// Atleast one of EXTENSION_BLOCK_FILTERS must get added to attributes
|
// Atleast one of EXTENSION_BLOCK_FILTERS must get added to attributes
|
||||||
};
|
};
|
||||||
|
|
||||||
// Any filter starting with '/' is interpreted as a regex. So if an attribute
|
|
||||||
// starts with a '/' it must be checked via a regex.
|
|
||||||
function regExpCheck(attr) {
|
|
||||||
return attr.startsWith("/") ? parseRegExp(attr) : attr;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let filter of EXTENSION_BLOCK_FILTERS) {
|
for (let filter of EXTENSION_BLOCK_FILTERS) {
|
||||||
let attr = blocklistElement.getAttribute(filter);
|
let attr = blocklistElement.getAttribute(filter);
|
||||||
if (attr)
|
if (attr) {
|
||||||
blockEntry.attributes.set(filter, regExpCheck(attr));
|
// Any filter starting with '/' is interpreted as a regex. So if an attribute
|
||||||
|
// starts with a '/' it must be checked via a regex.
|
||||||
|
if (attr.startsWith("/")) {
|
||||||
|
let lastSlash = attr.lastIndexOf("/");
|
||||||
|
let pattern = attr.slice(1, lastSlash);
|
||||||
|
let flags = attr.slice(lastSlash + 1);
|
||||||
|
blockEntry.attributes[filter] = new RegExp(pattern, flags);
|
||||||
|
} else {
|
||||||
|
blockEntry.attributes[filter] = attr;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var children = blocklistElement.children;
|
var children = blocklistElement.children;
|
||||||
|
|
||||||
for (let childElement of children) {
|
for (let childElement of children) {
|
||||||
if (childElement.localName === "prefs") {
|
let localName = childElement.localName;
|
||||||
|
if (localName == "prefs" && childElement.hasChildNodes) {
|
||||||
let prefElements = childElement.children;
|
let prefElements = childElement.children;
|
||||||
for (let prefElement of prefElements) {
|
for (let prefElement of prefElements) {
|
||||||
if (prefElement.localName == "pref") {
|
if (prefElement.localName == "pref") {
|
||||||
blockEntry.prefs.push(prefElement.textContent);
|
blockEntry.prefs.push(prefElement.textContent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (childElement.localName === "versionRange") {
|
} else if (localName == "versionRange") {
|
||||||
blockEntry.versions.push(new BlocklistItemData(childElement));
|
blockEntry.versions.push(new BlocklistItemData(childElement));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -975,19 +980,23 @@ var Blocklist = {
|
||||||
};
|
};
|
||||||
var hasMatch = false;
|
var hasMatch = false;
|
||||||
for (let childElement of children) {
|
for (let childElement of children) {
|
||||||
if (childElement.localName == "match") {
|
switch (childElement.localName) {
|
||||||
var name = childElement.getAttribute("name");
|
case "match":
|
||||||
var exp = childElement.getAttribute("exp");
|
var name = childElement.getAttribute("name");
|
||||||
try {
|
var exp = childElement.getAttribute("exp");
|
||||||
blockEntry.matches[name] = new RegExp(exp, "m");
|
try {
|
||||||
hasMatch = true;
|
blockEntry.matches[name] = new RegExp(exp, "m");
|
||||||
} catch (e) {
|
hasMatch = true;
|
||||||
// Ignore invalid regular expressions
|
} catch (e) {
|
||||||
}
|
// Ignore invalid regular expressions
|
||||||
} else if (childElement.localName == "versionRange") {
|
}
|
||||||
blockEntry.versions.push(new BlocklistItemData(childElement));
|
break;
|
||||||
} else if (childElement.localName == "infoURL") {
|
case "versionRange":
|
||||||
blockEntry.infoURL = childElement.textContent;
|
blockEntry.versions.push(new BlocklistItemData(childElement));
|
||||||
|
break;
|
||||||
|
case "infoURL":
|
||||||
|
blockEntry.infoURL = childElement.textContent;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Plugin entries require *something* to match to an actual plugin
|
// Plugin entries require *something* to match to an actual plugin
|
||||||
|
@ -1095,11 +1104,11 @@ var Blocklist = {
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
|
// Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
|
||||||
if (!appVersion && !gApp.version)
|
if (!appVersion && !gAppVersion)
|
||||||
return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
|
return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
|
||||||
|
|
||||||
if (!appVersion)
|
if (!appVersion)
|
||||||
appVersion = gApp.version;
|
appVersion = gAppVersion;
|
||||||
if (!toolkitVersion)
|
if (!toolkitVersion)
|
||||||
toolkitVersion = gApp.platformVersion;
|
toolkitVersion = gApp.platformVersion;
|
||||||
|
|
||||||
|
@ -1409,35 +1418,35 @@ var Blocklist = {
|
||||||
* Helper for constructing a blocklist.
|
* Helper for constructing a blocklist.
|
||||||
*/
|
*/
|
||||||
function BlocklistItemData(versionRangeElement) {
|
function BlocklistItemData(versionRangeElement) {
|
||||||
var versionRange = this.getBlocklistVersionRange(versionRangeElement);
|
this.targetApps = {};
|
||||||
this.minVersion = versionRange.minVersion;
|
let foundTarget = false;
|
||||||
this.maxVersion = versionRange.maxVersion;
|
this.severity = DEFAULT_SEVERITY;
|
||||||
if (versionRangeElement && versionRangeElement.hasAttribute("severity"))
|
this.vulnerabilityStatus = VULNERABILITYSTATUS_NONE;
|
||||||
this.severity = versionRangeElement.getAttribute("severity");
|
|
||||||
else
|
|
||||||
this.severity = DEFAULT_SEVERITY;
|
|
||||||
if (versionRangeElement && versionRangeElement.hasAttribute("vulnerabilitystatus")) {
|
|
||||||
this.vulnerabilityStatus = versionRangeElement.getAttribute("vulnerabilitystatus");
|
|
||||||
} else {
|
|
||||||
this.vulnerabilityStatus = VULNERABILITYSTATUS_NONE;
|
|
||||||
}
|
|
||||||
this.targetApps = { };
|
|
||||||
var found = false;
|
|
||||||
|
|
||||||
if (versionRangeElement) {
|
if (versionRangeElement) {
|
||||||
for (let targetAppElement of versionRangeElement.children) {
|
let versionRange = this.getBlocklistVersionRange(versionRangeElement);
|
||||||
if (targetAppElement.localName != "targetApplication")
|
this.minVersion = versionRange.minVersion;
|
||||||
continue;
|
this.maxVersion = versionRange.maxVersion;
|
||||||
found = true;
|
if (versionRangeElement.hasAttribute("severity"))
|
||||||
// default to the current application if id is not provided.
|
this.severity = versionRangeElement.getAttribute("severity");
|
||||||
var appID = targetAppElement.hasAttribute("id") ? targetAppElement.getAttribute("id") : gApp.ID;
|
if (versionRangeElement.hasAttribute("vulnerabilitystatus")) {
|
||||||
this.targetApps[appID] = this.getBlocklistAppVersions(targetAppElement);
|
this.vulnerabilityStatus = versionRangeElement.getAttribute("vulnerabilitystatus");
|
||||||
}
|
}
|
||||||
|
for (let targetAppElement of versionRangeElement.children) {
|
||||||
|
if (targetAppElement.localName == "targetApplication") {
|
||||||
|
foundTarget = true;
|
||||||
|
// default to the current application if id is not provided.
|
||||||
|
let appID = targetAppElement.id || gAppID;
|
||||||
|
this.targetApps[appID] = this.getBlocklistAppVersions(targetAppElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.minVersion = this.maxVersion = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to all versions of the current application when no targetApplication
|
// Default to all versions of the current application when no targetApplication
|
||||||
// elements were found
|
// elements were found
|
||||||
if (!found)
|
if (!foundTarget)
|
||||||
this.targetApps[gApp.ID] = this.getBlocklistAppVersions(null);
|
this.targetApps[gAppID] = [{minVersion: null, maxVersion: null}];
|
||||||
}
|
}
|
||||||
|
|
||||||
BlocklistItemData.prototype = {
|
BlocklistItemData.prototype = {
|
||||||
|
@ -1466,7 +1475,7 @@ BlocklistItemData.prototype = {
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Check if the application version matches
|
// Check if the application version matches
|
||||||
if (this.matchesTargetRange(gApp.ID, appVersion))
|
if (this.matchesTargetRange(gAppID, appVersion))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Check if the toolkit version matches
|
// Check if the toolkit version matches
|
||||||
|
@ -1541,7 +1550,7 @@ BlocklistItemData.prototype = {
|
||||||
// return minVersion = null and maxVersion = null if no specific versionRange
|
// return minVersion = null and maxVersion = null if no specific versionRange
|
||||||
// elements were found
|
// elements were found
|
||||||
if (appVersions.length == 0)
|
if (appVersions.length == 0)
|
||||||
appVersions.push(this.getBlocklistVersionRange(null));
|
appVersions.push({minVersion: null, maxVersion: null});
|
||||||
return appVersions;
|
return appVersions;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1558,15 +1567,9 @@ BlocklistItemData.prototype = {
|
||||||
* "maxVersion" The maximum version in a version range (default = null).
|
* "maxVersion" The maximum version in a version range (default = null).
|
||||||
*/
|
*/
|
||||||
getBlocklistVersionRange(versionRangeElement) {
|
getBlocklistVersionRange(versionRangeElement) {
|
||||||
var minVersion = null;
|
// getAttribute returns null if the attribute is not present.
|
||||||
var maxVersion = null;
|
let minVersion = versionRangeElement.getAttribute("minVersion");
|
||||||
if (!versionRangeElement)
|
let maxVersion = versionRangeElement.getAttribute("maxVersion");
|
||||||
return { minVersion, maxVersion };
|
|
||||||
|
|
||||||
if (versionRangeElement.hasAttribute("minVersion"))
|
|
||||||
minVersion = versionRangeElement.getAttribute("minVersion");
|
|
||||||
if (versionRangeElement.hasAttribute("maxVersion"))
|
|
||||||
maxVersion = versionRangeElement.getAttribute("maxVersion");
|
|
||||||
|
|
||||||
return { minVersion, maxVersion };
|
return { minVersion, maxVersion };
|
||||||
}
|
}
|
||||||
|
|
|
@ -743,8 +743,13 @@ var AddonTestUtils = {
|
||||||
throw new Error("Attempting to startup manager that was already started.");
|
throw new Error("Attempting to startup manager that was already started.");
|
||||||
|
|
||||||
|
|
||||||
if (newVersion)
|
if (newVersion) {
|
||||||
this.appInfo.version = newVersion;
|
this.appInfo.version = newVersion;
|
||||||
|
if (Cu.isModuleLoaded("resource://gre/modules/Blocklist.jsm")) {
|
||||||
|
let bsPassBlocklist = ChromeUtils.import("resource://gre/modules/Blocklist.jsm", {});
|
||||||
|
Object.defineProperty(bsPassBlocklist, "gAppVersion", {value: newVersion});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let XPIScope = ChromeUtils.import("resource://gre/modules/addons/XPIProvider.jsm", null);
|
let XPIScope = ChromeUtils.import("resource://gre/modules/addons/XPIProvider.jsm", null);
|
||||||
XPIScope.AsyncShutdown = MockAsyncShutdown;
|
XPIScope.AsyncShutdown = MockAsyncShutdown;
|
||||||
|
|
|
@ -791,6 +791,8 @@ add_task(async function update_schema_2() {
|
||||||
|
|
||||||
await changeXPIDBVersion(100);
|
await changeXPIDBVersion(100);
|
||||||
gAppInfo.version = "2";
|
gAppInfo.version = "2";
|
||||||
|
let bsPassBlocklist = ChromeUtils.import("resource://gre/modules/Blocklist.jsm", {});
|
||||||
|
Object.defineProperty(bsPassBlocklist, "gAppVersion", {value: "2"});
|
||||||
await promiseStartupManager();
|
await promiseStartupManager();
|
||||||
|
|
||||||
let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS);
|
let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS);
|
||||||
|
@ -815,6 +817,8 @@ add_task(async function update_schema_3() {
|
||||||
await promiseShutdownManager();
|
await promiseShutdownManager();
|
||||||
await changeXPIDBVersion(100);
|
await changeXPIDBVersion(100);
|
||||||
gAppInfo.version = "2.5";
|
gAppInfo.version = "2.5";
|
||||||
|
let bsPassBlocklist = ChromeUtils.import("resource://gre/modules/Blocklist.jsm", {});
|
||||||
|
Object.defineProperty(bsPassBlocklist, "gAppVersion", {value: "2.5"});
|
||||||
await promiseStartupManager();
|
await promiseStartupManager();
|
||||||
|
|
||||||
let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS);
|
let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS);
|
||||||
|
@ -848,6 +852,8 @@ add_task(async function update_schema_5() {
|
||||||
|
|
||||||
await changeXPIDBVersion(100);
|
await changeXPIDBVersion(100);
|
||||||
gAppInfo.version = "1";
|
gAppInfo.version = "1";
|
||||||
|
let bsPassBlocklist = ChromeUtils.import("resource://gre/modules/Blocklist.jsm", {});
|
||||||
|
Object.defineProperty(bsPassBlocklist, "gAppVersion", {value: "1"});
|
||||||
await promiseStartupManager();
|
await promiseStartupManager();
|
||||||
|
|
||||||
let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS);
|
let [s1, s2, s3, s4, h, r] = await promiseAddonsByIDs(ADDON_IDS);
|
||||||
|
|
Загрузка…
Ссылка в новой задаче