2011-06-07 12:11:37 -07:00
|
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
|
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
|
|
|
*
|
|
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
|
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
|
|
|
* the License. You may obtain a copy of the License at
|
|
|
|
* http://www.mozilla.org/MPL/
|
|
|
|
*
|
|
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
|
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
|
|
|
* for the specific language governing rights and limitations under the
|
|
|
|
* License.
|
|
|
|
*
|
|
|
|
* The Original Code is Mozilla Mobile Browser.
|
|
|
|
*
|
|
|
|
* The Initial Developer of the Original Code is Mozilla.
|
|
|
|
* Portions created by the Initial Developer are Copyright (C) 2011
|
|
|
|
* the Initial Developer. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Contributor(s):
|
|
|
|
* Mark Finkle <mfinkle@mozilla.com>
|
|
|
|
* Wes Johnston <wjohnston@mozilla.com>
|
|
|
|
*
|
|
|
|
* Alternatively, the contents of this file may be used under the terms of
|
|
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
|
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
|
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
|
|
|
* of those above. If you wish to allow use of your version of this file only
|
|
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
|
|
|
* use your version of this file under the terms of the MPL, indicate your
|
|
|
|
* decision by deleting the provisions above and replace them with the notice
|
|
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
|
|
|
* the provisions above, a recipient may use your version of this file under
|
|
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
|
|
|
*
|
|
|
|
* ***** END LICENSE BLOCK ***** */
|
|
|
|
|
|
|
|
let EXPORTED_SYMBOLS = ["LocaleRepository"];
|
|
|
|
|
|
|
|
const Cc = Components.classes;
|
|
|
|
const Ci = Components.interfaces;
|
|
|
|
const Cu = Components.utils;
|
|
|
|
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
Cu.import("resource://gre/modules/AddonManager.jsm");
|
|
|
|
Cu.import("resource://gre/modules/NetUtil.jsm");
|
|
|
|
|
|
|
|
// A map between XML keys to LocaleSearchResult keys for string values
|
|
|
|
// that require no extra parsing from XML
|
|
|
|
const STRING_KEY_MAP = {
|
|
|
|
name: "name",
|
|
|
|
target_locale: "targetLocale",
|
|
|
|
version: "version",
|
|
|
|
icon: "iconURL",
|
|
|
|
homepage: "homepageURL",
|
2011-08-15 10:42:50 -07:00
|
|
|
support: "supportURL",
|
|
|
|
strings: "strings"
|
2011-06-07 12:11:37 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
var LocaleRepository = {
|
|
|
|
loggingEnabled: false,
|
|
|
|
|
|
|
|
log: function(aMessage) {
|
|
|
|
if (this.loggingEnabled)
|
|
|
|
dump(aMessage + "\n");
|
|
|
|
},
|
|
|
|
|
|
|
|
_getUniqueDescendant: function _getUniqueDescendant(aElement, aTagName) {
|
|
|
|
let elementsList = aElement.getElementsByTagName(aTagName);
|
|
|
|
return (elementsList.length == 1) ? elementsList[0] : null;
|
|
|
|
},
|
|
|
|
|
|
|
|
_getTextContent: function _getTextContent(aElement) {
|
|
|
|
let textContent = aElement.textContent.trim();
|
|
|
|
return (textContent.length > 0) ? textContent : null;
|
|
|
|
},
|
|
|
|
|
|
|
|
_getDescendantTextContent: function _getDescendantTextContent(aElement, aTagName) {
|
|
|
|
let descendant = this._getUniqueDescendant(aElement, aTagName);
|
|
|
|
return (descendant != null) ? this._getTextContent(descendant) : null;
|
|
|
|
},
|
|
|
|
|
2011-08-15 10:42:52 -07:00
|
|
|
getLocales: function getLocales(aCallback, aFilters) {
|
2011-09-29 12:17:26 -07:00
|
|
|
let url = Services.prefs.getCharPref("extensions.getLocales.get.url");
|
2011-08-15 10:42:52 -07:00
|
|
|
|
|
|
|
if (!url) {
|
2011-06-07 12:11:37 -07:00
|
|
|
aCallback([]);
|
|
|
|
return;
|
|
|
|
}
|
2011-08-15 10:42:52 -07:00
|
|
|
|
|
|
|
let buildID = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo).QueryInterface(Ci.nsIXULRuntime).appBuildID;
|
|
|
|
if (aFilters) {
|
|
|
|
if (aFilters.buildID)
|
2011-09-29 12:17:35 -07:00
|
|
|
buildID = aFilters.buildID;
|
2011-08-15 10:42:52 -07:00
|
|
|
}
|
|
|
|
buildID = buildID.substring(0,4) + "-" + buildID.substring(4).replace(/\d{2}(?=\d)/g, "$&-");
|
|
|
|
url = url.replace(/%BUILDID_EXPANDED%/g, buildID);
|
|
|
|
url = Services.urlFormatter.formatURL(url);
|
|
|
|
|
2011-06-07 12:11:37 -07:00
|
|
|
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
|
|
|
|
request.mozBackgroundRequest = true;
|
2011-08-15 10:42:52 -07:00
|
|
|
request.open("GET", url, true);
|
2011-06-07 12:11:37 -07:00
|
|
|
request.overrideMimeType("text/xml");
|
|
|
|
|
|
|
|
let self = this;
|
2011-09-29 09:06:36 -07:00
|
|
|
request.addEventListener("readystatechange", function () {
|
2011-06-07 12:11:37 -07:00
|
|
|
if (request.readyState == 4) {
|
|
|
|
if (request.status == 200) {
|
|
|
|
self.log("---- got response")
|
|
|
|
let documentElement = request.responseXML.documentElement;
|
|
|
|
let elements = documentElement.getElementsByTagName("addon");
|
|
|
|
let totalResults = elements.length;
|
|
|
|
let parsedTotalResults = parseInt(documentElement.getAttribute("total_results"));
|
|
|
|
if (parsedTotalResults >= totalResults)
|
|
|
|
totalResults = parsedTotalResults;
|
|
|
|
|
|
|
|
// TODO: Create a real Skip object from installed locales
|
|
|
|
self._parseLocales(elements, totalResults, { ids: [], sourceURIs: [] }, aCallback);
|
|
|
|
} else {
|
|
|
|
Cu.reportError("Locale Repository: Error getting locale from AMO [" + request.status + "]");
|
|
|
|
}
|
|
|
|
}
|
2011-09-29 09:06:36 -07:00
|
|
|
}, false);
|
2011-06-07 12:11:37 -07:00
|
|
|
|
|
|
|
request.send(null);
|
|
|
|
},
|
|
|
|
|
|
|
|
_parseLocale: function _parseLocale(aElement, aSkip) {
|
|
|
|
let skipIDs = (aSkip && aSkip.ids) ? aSkip.ids : [];
|
|
|
|
let skipSourceURIs = (aSkip && aSkip.sourceURIs) ? aSkip.sourceURIs : [];
|
|
|
|
|
|
|
|
let guid = this._getDescendantTextContent(aElement, "guid");
|
|
|
|
if (guid == null || skipIDs.indexOf(guid) != -1)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
let addon = new LocaleSearchResult(guid);
|
|
|
|
let result = {
|
|
|
|
addon: addon,
|
|
|
|
xpiURL: null,
|
|
|
|
xpiHash: null
|
|
|
|
};
|
|
|
|
|
|
|
|
let self = this;
|
|
|
|
for (let node = aElement.firstChild; node; node = node.nextSibling) {
|
|
|
|
if (!(node instanceof Ci.nsIDOMElement))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
let localName = node.localName;
|
|
|
|
|
|
|
|
// Handle case where the wanted string value is located in text content
|
|
|
|
// but only if the content is not empty
|
|
|
|
if (localName in STRING_KEY_MAP) {
|
|
|
|
addon[STRING_KEY_MAP[localName]] = this._getTextContent(node) || addon[STRING_KEY_MAP[localName]];
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle cases that aren't as simple as grabbing the text content
|
|
|
|
switch (localName) {
|
|
|
|
case "type":
|
|
|
|
// Map AMO's type id to corresponding string
|
|
|
|
let id = parseInt(node.getAttribute("id"));
|
|
|
|
switch (id) {
|
|
|
|
case 5:
|
|
|
|
addon.type = "language";
|
|
|
|
break;
|
|
|
|
default:
|
2011-09-29 12:17:26 -07:00
|
|
|
this.log("Unknown type id when parsing addon: " + id);
|
2011-06-07 12:11:37 -07:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
case "authors":
|
|
|
|
let authorNodes = node.getElementsByTagName("author");
|
|
|
|
Array.forEach(authorNodes, function(aAuthorNode) {
|
|
|
|
let name = self._getDescendantTextContent(aAuthorNode, "name");
|
|
|
|
if (name == null)
|
|
|
|
name = self._getTextContent(aAuthorNode);
|
|
|
|
let link = self._getDescendantTextContent(aAuthorNode, "link");
|
|
|
|
if (name == null && link == null)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let author = { name: name, link: link };
|
|
|
|
if (addon.creator == null) {
|
|
|
|
addon.creator = author;
|
|
|
|
} else {
|
|
|
|
if (addon.developers == null)
|
|
|
|
addon.developers = [];
|
|
|
|
|
|
|
|
addon.developers.push(author);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case "status":
|
|
|
|
let repositoryStatus = parseInt(node.getAttribute("id"));
|
|
|
|
if (!isNaN(repositoryStatus))
|
|
|
|
addon.repositoryStatus = repositoryStatus;
|
|
|
|
break;
|
|
|
|
case "all_compatible_os":
|
|
|
|
let nodes = node.getElementsByTagName("os");
|
|
|
|
addon.isPlatformCompatible = Array.some(nodes, function(aNode) {
|
|
|
|
let text = aNode.textContent.toLowerCase().trim();
|
|
|
|
return text == "all" || text == Services.appinfo.OS.toLowerCase();
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case "install":
|
|
|
|
// No os attribute means the xpi is compatible with any os
|
|
|
|
if (node.hasAttribute("os") && node.getAttribute("os")) {
|
|
|
|
let os = node.getAttribute("os").trim().toLowerCase();
|
|
|
|
// If the os is not ALL and not the current OS then ignore this xpi
|
|
|
|
if (os != "all" && os != Services.appinfo.OS.toLowerCase())
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
let xpiURL = this._getTextContent(node);
|
|
|
|
if (xpiURL == null)
|
|
|
|
break;
|
|
|
|
|
|
|
|
if (skipSourceURIs.indexOf(xpiURL) != -1)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
result.xpiURL = xpiURL;
|
2011-09-29 12:17:26 -07:00
|
|
|
try {
|
|
|
|
addon.sourceURI = NetUtil.newURI(xpiURL);
|
|
|
|
} catch(ex) {
|
|
|
|
this.log("Addon has invalid uri: " + addon.sourceURI);
|
|
|
|
addon.sourceURI = null;
|
|
|
|
}
|
2011-06-07 12:11:37 -07:00
|
|
|
|
|
|
|
let size = parseInt(node.getAttribute("size"));
|
|
|
|
addon.size = (size >= 0) ? size : null;
|
|
|
|
|
|
|
|
let xpiHash = node.getAttribute("hash");
|
|
|
|
if (xpiHash != null)
|
|
|
|
xpiHash = xpiHash.trim();
|
|
|
|
result.xpiHash = xpiHash ? xpiHash : null;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
},
|
|
|
|
|
|
|
|
_parseLocales: function _parseLocales(aElements, aTotalResults, aSkip, aCallback) {
|
|
|
|
let self = this;
|
|
|
|
let results = [];
|
|
|
|
for (let i = 0; i < aElements.length; i++) {
|
|
|
|
let element = aElements[i];
|
|
|
|
|
|
|
|
// Ignore add-ons not compatible with this Application
|
|
|
|
let tags = this._getUniqueDescendant(element, "compatible_applications");
|
|
|
|
if (tags == null)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
let applications = tags.getElementsByTagName("appID");
|
|
|
|
let compatible = Array.some(applications, function(aAppNode) {
|
|
|
|
if (self._getTextContent(aAppNode) != Services.appinfo.ID)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
let parent = aAppNode.parentNode;
|
|
|
|
let minVersion = self._getDescendantTextContent(parent, "min_version");
|
|
|
|
let maxVersion = self._getDescendantTextContent(parent, "max_version");
|
|
|
|
if (minVersion == null || maxVersion == null)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
let currentVersion = Services.appinfo.version;
|
|
|
|
return (Services.vc.compare(minVersion, currentVersion) <= 0 && Services.vc.compare(currentVersion, maxVersion) <= 0);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!compatible)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// Add-on meets all requirements, so parse out data
|
|
|
|
let result = this._parseLocale(element, aSkip);
|
|
|
|
if (result == null)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// Ignore add-on missing a required attribute
|
2011-09-29 12:17:26 -07:00
|
|
|
let requiredAttributes = ["id", "name", "version", "type", "targetLocale", "sourceURI"];
|
2011-06-07 12:11:37 -07:00
|
|
|
if (requiredAttributes.some(function(aAttribute) !result.addon[aAttribute]))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// Add only if the add-on is compatible with the platform
|
|
|
|
if (!result.addon.isPlatformCompatible)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// Add only if there was an xpi compatible with this OS
|
|
|
|
if (!result.xpiURL)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
results.push(result);
|
|
|
|
|
|
|
|
// Ignore this add-on from now on by adding it to the skip array
|
|
|
|
aSkip.ids.push(result.addon.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Immediately report success if no AddonInstall instances to create
|
|
|
|
let pendingResults = results.length;
|
|
|
|
if (pendingResults == 0) {
|
|
|
|
aCallback([]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create an AddonInstall for each result
|
|
|
|
let self = this;
|
|
|
|
results.forEach(function(aResult) {
|
|
|
|
let addon = aResult.addon;
|
|
|
|
let callback = function(aInstall) {
|
|
|
|
aResult.addon.install = aInstall;
|
|
|
|
pendingResults--;
|
|
|
|
if (pendingResults == 0)
|
|
|
|
aCallback(results);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aResult.xpiURL) {
|
|
|
|
AddonManager.getInstallForURL(aResult.xpiURL, callback,
|
|
|
|
"application/x-xpinstall", aResult.xpiHash,
|
|
|
|
addon.name, addon.iconURL, addon.version);
|
|
|
|
} else {
|
|
|
|
callback(null);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
function LocaleSearchResult(aId) {
|
|
|
|
this.id = aId;
|
|
|
|
}
|
|
|
|
|
|
|
|
LocaleSearchResult.prototype = {
|
|
|
|
id: null,
|
|
|
|
type: null,
|
|
|
|
targetLocale: null,
|
|
|
|
name: null,
|
|
|
|
addon: null,
|
|
|
|
version: null,
|
|
|
|
iconURL: null,
|
|
|
|
install: null,
|
|
|
|
sourceURI: null,
|
|
|
|
repositoryStatus: null,
|
|
|
|
size: null,
|
2011-08-15 10:42:50 -07:00
|
|
|
strings: "",
|
2011-06-07 12:11:37 -07:00
|
|
|
updateDate: null,
|
|
|
|
isCompatible: true,
|
|
|
|
isPlatformCompatible: true,
|
|
|
|
providesUpdatesSecurely: true,
|
|
|
|
blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
|
|
|
|
appDisabled: false,
|
|
|
|
userDisabled: false,
|
|
|
|
scope: AddonManager.SCOPE_PROFILE,
|
|
|
|
isActive: true,
|
|
|
|
pendingOperations: AddonManager.PENDING_NONE,
|
|
|
|
permissions: 0
|
|
|
|
};
|