зеркало из https://github.com/mozilla/pjs.git
Bug 306478 - Extension manager should use xpinstall crypto hashes. r=bsmedberg
This commit is contained in:
Родитель
39d6430f14
Коммит
89156751b9
|
@ -319,10 +319,9 @@ XPInstallDownloadManager.prototype = {
|
|||
}
|
||||
|
||||
var type = isExtensions ? nsIUpdateItem.TYPE_EXTENSION : nsIUpdateItem.TYPE_THEME;
|
||||
// gExtensionManager.addDownload(displayName, url, iconURL, type);
|
||||
var item = Components.classes["@mozilla.org/updates/item;1"]
|
||||
.createInstance(Components.interfaces.nsIUpdateItem);
|
||||
item.init(url, " ", "app-profile", "", "", displayName, url, iconURL, "", type);
|
||||
item.init(url, " ", "app-profile", "", "", displayName, url, "", iconURL, "", type);
|
||||
items.push(item);
|
||||
|
||||
// Advance the enumerator
|
||||
|
|
|
@ -326,6 +326,7 @@ var gFoundPage = {
|
|||
checkbox.setAttribute("type", "update");
|
||||
checkbox.label = item.name + " " + item.version;
|
||||
checkbox.setAttribute("URL", item.xpiURL);
|
||||
checkbox.setAttribute("hash", item.xpiHash);
|
||||
checkbox.infoURL = "";
|
||||
checkbox.internalName = "";
|
||||
uri.spec = item.xpiURL;
|
||||
|
@ -407,6 +408,7 @@ var gInstallingPage = {
|
|||
// Get XPInstallManager and kick off download/install
|
||||
// process, registering us as an observer.
|
||||
var items = [];
|
||||
var hashes = [];
|
||||
this._objs = [];
|
||||
|
||||
this._restartRequired = false;
|
||||
|
@ -418,13 +420,14 @@ var gInstallingPage = {
|
|||
for (var i = 0; i < checkboxes.length; ++i) {
|
||||
if (checkboxes[i].type == "update" && checkboxes[i].checked) {
|
||||
items.push(checkboxes[i].URL);
|
||||
hashes.push(checkboxes[i].hash);
|
||||
this._objs.push({ name: checkboxes[i].label });
|
||||
}
|
||||
}
|
||||
|
||||
var xpimgr = Components.classes["@mozilla.org/xpinstall/install-manager;1"]
|
||||
.createInstance(Components.interfaces.nsIXPInstallManager);
|
||||
xpimgr.initManagerFromChrome(items, items.length, this);
|
||||
xpimgr.initManagerWithHashes(items, hashes, items.length, this);
|
||||
},
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -1,217 +0,0 @@
|
|||
<?xml version="1.0"?>
|
||||
|
||||
<!DOCTYPE bindings SYSTEM "chrome://mozapps/locale/extensions/update.dtd">
|
||||
|
||||
<bindings id="updatesBindings"
|
||||
xmlns="http://www.mozilla.org/xbl"
|
||||
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:xbl="http://www.mozilla.org/xbl">
|
||||
|
||||
<binding id="updateCategorySet" extends="chrome://global/content/bindings/radio.xml#radiogroup">
|
||||
<implementation>
|
||||
<method name="computeSizes">
|
||||
<body>
|
||||
<![CDATA[
|
||||
var kids = this._getRadioChildren();
|
||||
for (var i = 0; i < kids.length; ++i)
|
||||
kids[i].expandedHeight = kids[i]._content.boxObject.height;
|
||||
this.removeAttribute("_uninitialized");
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
<field name="lastSelectedItem">null</field>
|
||||
</implementation>
|
||||
</binding>
|
||||
|
||||
<binding id="updateCategory" extends="chrome://global/content/bindings/radio.xml#radio">
|
||||
<resources>
|
||||
<stylesheet src="chrome://global/skin/radio.css"/>
|
||||
<stylesheet src="chrome://mozapps/skin/update/update.css"/>
|
||||
</resources>
|
||||
<content>
|
||||
<xul:hbox class="updateCategoryBox" xbl:inherits="selected,checked,disabled">
|
||||
#ifdef MOZ_WIDGET_GTK2
|
||||
<xul:hbox class="radio-spacer-box">
|
||||
#endif
|
||||
<xul:hbox class="radio-check-box1" xbl:inherits="selected,checked,disabled">
|
||||
<xul:hbox class="radio-check-box2" flex="1">
|
||||
<xul:image class="radio-check" xbl:inherits="selected,checked,disabled"/>
|
||||
</xul:hbox>
|
||||
</xul:hbox>
|
||||
#ifdef MOZ_WIDGET_GTK2
|
||||
</xul:hbox>
|
||||
#endif
|
||||
<xul:image class="updateCategoryIcon" xbl:inherits="src"/>
|
||||
<xul:label class="updateCategoryLabel" xbl:inherits="xbl:text=label,accesskey,crop,selected" flex="1"/>
|
||||
</xul:hbox>
|
||||
<xul:vbox flex="1" class="updateCategoryContent">
|
||||
<children/>
|
||||
</xul:vbox>
|
||||
</content>
|
||||
<implementation implements="nsITimerCallback">
|
||||
<property name="expandedHeight"
|
||||
onget="return this.getAttribute('expandedHeight');"
|
||||
onset="this.setAttribute('expandedHeight', val); return val;"/>
|
||||
|
||||
<method name="notify">
|
||||
<parameter name="aTimer"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
var newHeight;
|
||||
if (this._destinationSize == 0) {
|
||||
if (this._content.boxObject.height > 0) {
|
||||
newHeight = this._content.boxObject.height - this._animateIncrement;
|
||||
newHeight = newHeight < 0 ? 0 : newHeight;
|
||||
this._content.style.height = newHeight + "px";
|
||||
this._timer.initWithCallback(this, this._animateDelay,
|
||||
Components.interfaces.nsITimer.TYPE_ONE_SHOT);
|
||||
}
|
||||
else {
|
||||
this._timer.cancel();
|
||||
this._content.style.visibility = "collapse";
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (this._content.boxObject.height <= this._destinationSize) {
|
||||
newHeight = this._content.boxObject.height + this._animateIncrement;
|
||||
newHeight = newHeight > this.expandedHeight ? this.expandedHeight : newHeight;
|
||||
this._content.style.height = newHeight + "px";
|
||||
this._timer.initWithCallback(this, this._animateDelay,
|
||||
Components.interfaces.nsITimer.TYPE_ONE_SHOT);
|
||||
}
|
||||
else
|
||||
this._timer.cancel();
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="_setUpTimer">
|
||||
<parameter name="aSelected"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (!this._timer)
|
||||
this._timer = Components.classes["@mozilla.org/timer;1"]
|
||||
.createInstance(Components.interfaces.nsITimer);
|
||||
else
|
||||
this._timer.cancel();
|
||||
|
||||
this._content.style.visibility = "visible";
|
||||
|
||||
this._destinationSize = aSelected ? this.expandedHeight : 0;
|
||||
this._timer.initWithCallback(this, this._animateDelay,
|
||||
Components.interfaces.nsITimer.TYPE_ONE_SHOT);
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<field name="_content">
|
||||
document.getAnonymousElementByAttribute(this, "class", "updateCategoryContent");
|
||||
</field>
|
||||
<field name="_timer">null</field>
|
||||
<field name="_animateDelay">50</field>
|
||||
<field name="_animateIncrement">25</field>
|
||||
<field name="_destinationSize">0</field>
|
||||
</implementation>
|
||||
<handlers>
|
||||
<handler event="RadioStateChange">
|
||||
<![CDATA[
|
||||
/*
|
||||
this._content.style.height = "0px";
|
||||
|
||||
if (this.radioGroup.lastSelectedItem)
|
||||
this.radioGroup.lastSelectedItem._setUpTimer(false);
|
||||
this.radioGroup.lastSelectedItem = this;
|
||||
|
||||
this._setUpTimer(true);*/
|
||||
]]>
|
||||
</handler>
|
||||
</handlers>
|
||||
</binding>
|
||||
|
||||
<binding id="updateItem" extends="chrome://global/content/bindings/checkbox.xml#checkbox">
|
||||
<content>
|
||||
#ifdef MOZ_WIDGET_GTK2
|
||||
<xul:hbox class="checkbox-spacer-box">
|
||||
#endif
|
||||
<xul:image class="checkbox-check" xbl:inherits="checked,disabled"/>
|
||||
#ifdef MOZ_WIDGET_GTK2
|
||||
</xul:hbox>
|
||||
<xul:hbox class="checkbox-label-center-box" flex="1">
|
||||
#endif
|
||||
<xul:hbox class="checkbox-label-box" flex="1">
|
||||
<xul:label class="checkbox-label foundLabel" xbl:inherits="xbl:text=label,accesskey" flex="1"/>
|
||||
<xul:label class="checkbox-label" value="&from.label;"/>
|
||||
<xul:label class="checkbox-label foundSource" xbl:inherits="xbl:text=source,infoURL,accesskey,crop"/>
|
||||
</xul:hbox>
|
||||
#ifdef MOZ_WIDGET_GTK2
|
||||
</xul:hbox>
|
||||
#endif
|
||||
</content>
|
||||
<implementation>
|
||||
<property name="type"
|
||||
onget="return this.getAttribute('type');"
|
||||
onset="this.setAttribute('type', val); return val;"/>
|
||||
<property name="source"
|
||||
onget="return this.getAttribute('source');"
|
||||
onset="this.setAttribute('source', val); return val;"/>
|
||||
<property name="URL"
|
||||
onget="return this.getAttribute('URL');"
|
||||
onset="this.setAttribute('URL', val); return val;"/>
|
||||
<property name="infoURL"
|
||||
onget="return this.getAttribute('infoURL');"
|
||||
onset="this.setAttribute('infoURL', val); return val;"/>
|
||||
<property name="internalName"
|
||||
onget="return this.getAttribute('internalName');"
|
||||
onset="this.setAttribute('internalName', val); return val;"/>
|
||||
</implementation>
|
||||
</binding>
|
||||
|
||||
<binding id="link" extends="chrome://global/content/bindings/text.xml#text-base">
|
||||
<content>
|
||||
<xul:label xbl:inherits="value=label,crop" class="linkLabel" flex="1"/>
|
||||
</content>
|
||||
<implementation>
|
||||
<property name="href"
|
||||
onget="return this.getAttribute('href');"
|
||||
onset="this.setAttribute('href', val); return val;"/>
|
||||
</implementation>
|
||||
<handlers>
|
||||
<handler event="keypress" keycode="VK_ENTER" action="this.click()" />
|
||||
<handler event="keypress" keycode="VK_RETURN" action="this.click()" />
|
||||
<handler event="click">
|
||||
<![CDATA[
|
||||
if (event.button != 0)
|
||||
return;
|
||||
|
||||
# If we're not a browser, use the external protocol service to load the URI.
|
||||
#ifndef MOZ_PHOENIX
|
||||
var uri = Components.classes["@mozilla.org/network/io-service;1"]
|
||||
.getService(Components.interfaces.nsIIOService)
|
||||
.newURI(this.getAttribute("href"), null, null);
|
||||
|
||||
var protocolSvc = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
|
||||
.getService(Components.interfaces.nsIExternalProtocolService);
|
||||
if (protocolSvc.isExposedProtocol(uri.scheme))
|
||||
protocolSvc.loadUrl(uri);
|
||||
# If we're a browser, open a new browser window instead.
|
||||
#else
|
||||
var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
|
||||
.getService(Components.interfaces.nsIWindowWatcher);
|
||||
var ary = Components.classes["@mozilla.org/supports-array;1"]
|
||||
.createInstance(Components.interfaces.nsISupportsArray);
|
||||
var url = Components.classes["@mozilla.org/supports-string;1"]
|
||||
.createInstance(Components.interfaces.nsISupportsString);
|
||||
url.data = this.getAttribute("href")
|
||||
ary.AppendElement(url);
|
||||
|
||||
ww.openWindow(null, "chrome://browser/content/browser.xul",
|
||||
"_blank", "chrome,all,dialog=no", ary);
|
||||
#endif
|
||||
]]>
|
||||
</handler>
|
||||
</handlers>
|
||||
</binding>
|
||||
|
||||
</bindings>
|
||||
|
|
@ -372,7 +372,7 @@ interface nsIExtensionManager : nsISupports
|
|||
* XXXben work in progress, the name of this interface will change after the
|
||||
* update system is complete, probably to nsIAddon
|
||||
*/
|
||||
[scriptable, uuid(415edb0a-4f2d-485c-9e10-f262b065ab33)]
|
||||
[scriptable, uuid(7f952767-427f-402b-8114-f80c95d1980d)]
|
||||
interface nsIUpdateItem : nsISupports
|
||||
{
|
||||
/**
|
||||
|
@ -412,6 +412,13 @@ interface nsIUpdateItem : nsISupports
|
|||
*/
|
||||
readonly attribute AString xpiURL;
|
||||
|
||||
/**
|
||||
* The string Hash for the XPI file. Can be null and if supplied must be in
|
||||
* the format of "type:hash" (see the types in nsICryptoHash and
|
||||
* nsIXPInstallManager::initManagerWithHashes).
|
||||
*/
|
||||
readonly attribute AString xpiHash;
|
||||
|
||||
/**
|
||||
* The URL of the icon that can be shown for this item.
|
||||
*/
|
||||
|
@ -442,7 +449,7 @@ interface nsIUpdateItem : nsISupports
|
|||
void init(in AString id, in AString version,
|
||||
in AString installLocationKey, in AString minAppVersion,
|
||||
in AString maxAppVersion, in AString name,
|
||||
in AString downloadURL, in AString iconURL,
|
||||
in AString downloadURL, in AString xpiHash, in AString iconURL,
|
||||
in AString updateURL, in long type);
|
||||
|
||||
/**
|
||||
|
|
|
@ -287,11 +287,11 @@ function getResourceForID(id) {
|
|||
* ...
|
||||
*/
|
||||
function makeItem(id, version, locationKey, minVersion, maxVersion, name,
|
||||
updateURL, iconURL, updateRDF, type) {
|
||||
updateURL, updateHash, iconURL, updateRDF, type) {
|
||||
var item = Components.classes["@mozilla.org/updates/item;1"]
|
||||
.createInstance(Components.interfaces.nsIUpdateItem);
|
||||
item.init(id, version, locationKey, minVersion, maxVersion, name,
|
||||
updateURL, iconURL, updateRDF, type);
|
||||
updateURL, updateHash, iconURL, updateRDF, type);
|
||||
return item;
|
||||
}
|
||||
|
||||
|
@ -2501,6 +2501,7 @@ ExtensionManager.prototype = {
|
|||
targetAppInfo ? targetAppInfo.maxVersion : "",
|
||||
getManifestProperty(installManifest, "name"),
|
||||
"", /* XPI Update URL */
|
||||
"", /* XPI Update Hash */
|
||||
getManifestProperty(installManifest, "iconURL"),
|
||||
getManifestProperty(installManifest, "updateURL"),
|
||||
installData.type);
|
||||
|
@ -3849,7 +3850,10 @@ ExtensionManager.prototype = {
|
|||
aInstallLocationKey,
|
||||
installData.currentApp.minVersion,
|
||||
installData.currentApp.maxVersion,
|
||||
installData.name, "", "",
|
||||
installData.name,
|
||||
"", /* XPI Update URL */
|
||||
"", /* XPI Update Hash */
|
||||
"", /* Icon URL */
|
||||
installData.updateURL || "",
|
||||
installData.type);
|
||||
em.update([item], 1, true, this);
|
||||
|
@ -4121,6 +4125,7 @@ ExtensionManager.prototype = {
|
|||
installLocation : EM_L(installLocation.name),
|
||||
type : EM_I(type),
|
||||
availableUpdateURL : null,
|
||||
availableUpdateHash : null,
|
||||
availableUpdateVersion: null };
|
||||
for (var p in props)
|
||||
ds.setItemProperty(id, EM_R(p), props[p]);
|
||||
|
@ -4160,6 +4165,7 @@ ExtensionManager.prototype = {
|
|||
var props = { installLocation : EM_L(installLocation.name),
|
||||
type : EM_I(type),
|
||||
availableUpdateURL : null,
|
||||
availableUpdateHash : null,
|
||||
availableUpdateVersion : null };
|
||||
for (var p in props)
|
||||
ds.setItemProperty(id, EM_R(p), props[p]);
|
||||
|
@ -4683,6 +4689,7 @@ ExtensionManager.prototype = {
|
|||
this._downloadCount += itemCount;
|
||||
|
||||
var urls = [];
|
||||
var hashes = [];
|
||||
var txn = new ItemDownloadTransaction(this);
|
||||
for (var i = 0; i < itemCount; ++i) {
|
||||
var currItem = items[i];
|
||||
|
@ -4690,6 +4697,7 @@ ExtensionManager.prototype = {
|
|||
txn.addDownload(currItem, txnID);
|
||||
this._transactions.push(txn);
|
||||
urls.push(currItem.xpiURL);
|
||||
hashes.push(currItem.xpiHash);
|
||||
}
|
||||
|
||||
// Kick off the download process for this transaction
|
||||
|
@ -4701,7 +4709,7 @@ ExtensionManager.prototype = {
|
|||
var xpimgr =
|
||||
Components.classes["@mozilla.org/xpinstall/install-manager;1"].
|
||||
createInstance(Components.interfaces.nsIXPInstallManager);
|
||||
xpimgr.initManagerFromChrome(urls, urls.length, txn);
|
||||
xpimgr.initManagerWithHashes(urls, hashes, urls.length, txn);
|
||||
}
|
||||
else
|
||||
gOS.notifyObservers(txn, "xpinstall-progress", "open");
|
||||
|
@ -5386,7 +5394,7 @@ RDFItemUpdater.prototype = {
|
|||
|
||||
// Parse the response RDF
|
||||
function UpdateData() {};
|
||||
UpdateData.prototype = { version: "0.0", updateLink: null,
|
||||
UpdateData.prototype = { version: "0.0", updateLink: null, updateHash: null,
|
||||
minVersion: "0.0", maxVersion: "0.0" };
|
||||
|
||||
var versionUpdate = new UpdateData();
|
||||
|
@ -5414,7 +5422,10 @@ RDFItemUpdater.prototype = {
|
|||
newestUpdate.minVersion,
|
||||
newestUpdate.maxVersion,
|
||||
aLocalItem.name,
|
||||
newestUpdate.updateLink, "", "",
|
||||
newestUpdate.updateLink,
|
||||
newestUpdate.updateHash,
|
||||
"", /* Icon URL */
|
||||
"", /* RDF Update URL */
|
||||
aLocalItem.type);
|
||||
if (this._updater._isValidUpdate(aLocalItem, newerItem))
|
||||
++this._updater._updateCount;
|
||||
|
@ -5436,8 +5447,11 @@ RDFItemUpdater.prototype = {
|
|||
aLocalItem.installLocationKey,
|
||||
versionUpdate.minVersion,
|
||||
versionUpdate.maxVersion,
|
||||
aLocalItem.name,
|
||||
"", "", "",
|
||||
aLocalItem.name,
|
||||
"", /* XPI Update URL */
|
||||
"", /* XPI Update Hash */
|
||||
"", /* Icon URL */
|
||||
"", /* RDF Update URL */
|
||||
aLocalItem.type);
|
||||
if (this._updater._isValidUpdate(aLocalItem, sameItem)) {
|
||||
// Install-time updates are not written to the DS because there is no
|
||||
|
@ -5565,6 +5579,7 @@ RDFItemUpdater.prototype = {
|
|||
if (aVersionUpdatesOnly ? result == 0 : result > 0) {
|
||||
aUpdateData.version = version;
|
||||
aUpdateData.updateLink = this._getPropertyFromResource(aDataSource, targetApp, "updateLink", aLocalItem);
|
||||
aUpdateData.updateHash = this._getPropertyFromResource(aDataSource, targetApp, "updateHash", aLocalItem);
|
||||
aUpdateData.minVersion = this._getPropertyFromResource(aDataSource, targetApp, "minVersion", aLocalItem);
|
||||
aUpdateData.maxVersion = this._getPropertyFromResource(aDataSource, targetApp, "maxVersion", aLocalItem);
|
||||
}
|
||||
|
@ -5747,6 +5762,7 @@ ExtensionsDataSource.prototype = {
|
|||
return null;
|
||||
|
||||
var targetAppInfo = this.getTargetApplicationInfo(id, this);
|
||||
var updateHash = this.getItemProperty(id, "availableUpdateHash");
|
||||
return makeItem(id,
|
||||
this.getItemProperty(id, "version"),
|
||||
this.getItemProperty(id, "installLocation"),
|
||||
|
@ -5754,6 +5770,7 @@ ExtensionsDataSource.prototype = {
|
|||
targetAppInfo ? targetAppInfo.maxVersion : "",
|
||||
this.getItemProperty(id, "name"),
|
||||
this.getItemProperty(id, "availableUpdateURL"),
|
||||
updateHash ? updateHash : "",
|
||||
this.getItemProperty(id, "iconURL"),
|
||||
this.getItemProperty(id, "updateURL"),
|
||||
this.getItemProperty(id, "type"));
|
||||
|
@ -6483,15 +6500,18 @@ ExtensionsDataSource.prototype = {
|
|||
onAddonUpdateEnded: function(addon, status) {
|
||||
LOG("Datasource: Addon Update Ended: " + addon.id + ", status: " + status);
|
||||
this._updateURLs[addon.id] = status;
|
||||
var url = null, version = null;
|
||||
var url = null, hash = null, version = null;
|
||||
var updateAvailable = status == nsIAddonUpdateCheckListener.STATUS_UPDATE;
|
||||
if (updateAvailable) {
|
||||
url = EM_L(addon.xpiURL);
|
||||
if (addon.xpiHash)
|
||||
hash = EM_L(addon.xpiHash);
|
||||
version = EM_L(addon.version);
|
||||
}
|
||||
this.setItemProperty(addon.id, EM_R("availableUpdateURL"), url);
|
||||
this.updateProperty(addon.id, "availableUpdateURL");
|
||||
this.setItemProperty(addon.id, EM_R("availableUpdateHash"), hash);
|
||||
this.setItemProperty(addon.id, EM_R("availableUpdateVersion"), version);
|
||||
this.updateProperty(addon.id, "availableUpdateURL");
|
||||
this.updateProperty(addon.id, "displayDescription");
|
||||
},
|
||||
|
||||
|
@ -7015,7 +7035,7 @@ UpdateItem.prototype = {
|
|||
* See nsIUpdateService.idl
|
||||
*/
|
||||
init: function(id, version, installLocationKey, minAppVersion, maxAppVersion,
|
||||
name, downloadURL, iconURL, updateURL, type) {
|
||||
name, downloadURL, xpiHash, iconURL, updateURL, type) {
|
||||
this._id = id;
|
||||
this._version = version;
|
||||
this._installLocationKey = installLocationKey;
|
||||
|
@ -7023,6 +7043,7 @@ UpdateItem.prototype = {
|
|||
this._maxAppVersion = maxAppVersion;
|
||||
this._name = name;
|
||||
this._downloadURL = downloadURL;
|
||||
this._xpiHash = xpiHash;
|
||||
this._iconURL = iconURL;
|
||||
this._updateURL = updateURL;
|
||||
this._type = type;
|
||||
|
@ -7038,6 +7059,7 @@ UpdateItem.prototype = {
|
|||
get maxAppVersion() { return this._maxAppVersion; },
|
||||
get name() { return this._name; },
|
||||
get xpiURL() { return this._downloadURL; },
|
||||
get xpiHash() { return this._xpiHash; },
|
||||
get iconURL() { return this._iconURL },
|
||||
get updateRDF() { return this._updateURL; },
|
||||
get type() { return this._type; },
|
||||
|
@ -7053,6 +7075,7 @@ UpdateItem.prototype = {
|
|||
maxAppVersion : this._maxAppVersion,
|
||||
name : this._name,
|
||||
xpiURL : this._downloadURL,
|
||||
xpiHash : this._xpiHash,
|
||||
iconURL : this._iconURL,
|
||||
updateRDF : this._updateURL,
|
||||
type : this._type
|
||||
|
|
Загрузка…
Ссылка в новой задаче