gecko/toolkit/mozapps/extensions/nsAddonRepository.js

393 lines
13 KiB
JavaScript

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/*
# ***** 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 the Extension Manager.
#
# The Initial Developer of the Original Code is mozilla.org
# Portions created by the Initial Developer are Copyright (C) 2008
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Dave Townsend <dtownsend@oxymoronical.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 *****
*/
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/AddonManager.jsm");
const PREF_GETADDONS_BROWSEADDONS = "extensions.getAddons.browseAddons";
const PREF_GETADDONS_BROWSERECOMMENDED = "extensions.getAddons.recommended.browseURL";
const PREF_GETADDONS_GETRECOMMENDED = "extensions.getAddons.recommended.url";
const PREF_GETADDONS_BROWSESEARCHRESULTS = "extensions.getAddons.search.browseURL";
const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml";
const API_VERSION = "1.2";
function AddonSearchResult() {
}
AddonSearchResult.prototype = {
id: null,
name: null,
version: null,
summary: null,
description: null,
rating: null,
iconURL: null,
thumbnailURL: null,
homepageURL: null,
eula: null,
type: null,
xpiURL: null,
xpiHash: null,
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonSearchResult])
}
function AddonRepository() {
}
AddonRepository.prototype = {
// The current set of results
_addons: null,
// Whether we are currently searching or not
_searching: false,
// Is this a search for recommended add-ons
_recommended: false,
// XHR associated with the current request
_request: null,
// Callback object to notify on completion
_callback: null,
// Maximum number of results to return
_maxResults: null,
get homepageURL() {
return Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
.getService(Components.interfaces.nsIURLFormatter)
.formatURLPref(PREF_GETADDONS_BROWSEADDONS);
},
get isSearching() {
return this._searching;
},
getRecommendedURL: function() {
var urlf = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
.getService(Components.interfaces.nsIURLFormatter);
return urlf.formatURLPref(PREF_GETADDONS_BROWSERECOMMENDED);
},
getSearchURL: function(aSearchTerms) {
var prefs = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch);
var urlf = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
.getService(Components.interfaces.nsIURLFormatter);
var url = prefs.getCharPref(PREF_GETADDONS_BROWSESEARCHRESULTS);
url = url.replace(/%TERMS%/g, encodeURIComponent(aSearchTerms));
return urlf.formatURL(url);
},
cancelSearch: function() {
this._searching = false;
if (this._request) {
this._request.abort();
this._request = null;
}
this._callback = null;
this._addons = null;
},
retrieveRecommendedAddons: function(aMaxResults, aCallback) {
if (this._searching)
return;
this._searching = true;
this._addons = [];
this._callback = aCallback;
this._recommended = true;
this._maxResults = aMaxResults;
var prefs = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch);
var urlf = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
.getService(Components.interfaces.nsIURLFormatter);
var uri = prefs.getCharPref(PREF_GETADDONS_GETRECOMMENDED);
uri = uri.replace(/%API_VERSION%/g, API_VERSION);
uri = urlf.formatURL(uri);
this._loadList(uri);
},
searchAddons: function(aSearchTerms, aMaxResults, aCallback) {
if (this._searching)
return;
this._searching = true;
this._addons = [];
this._callback = aCallback;
this._recommended = false;
this._maxResults = aMaxResults;
var prefs = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch);
var urlf = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
.getService(Components.interfaces.nsIURLFormatter);
var uri = prefs.getCharPref(PREF_GETADDONS_GETSEARCHRESULTS);
uri = uri.replace(/%API_VERSION%/g, API_VERSION);
// We double encode due to bug 427155
uri = uri.replace(/%TERMS%/g, encodeURIComponent(encodeURIComponent(aSearchTerms)));
uri = urlf.formatURL(uri);
this._loadList(uri);
},
// Posts results to the callback
_reportSuccess: function(aCount) {
this._searching = false;
this._request = null;
// The callback may want to trigger a new search so clear references early
var addons = this._addons;
var callback = this._callback;
this._callback = null;
this._addons = null;
callback.searchSucceeded(addons, addons.length, this._recommended ? -1 : aCount);
},
// Notifies the callback of a failure
_reportFailure: function(aEvent) {
this._searching = false;
this._request = null;
// The callback may want to trigger a new search so clear references early
var callback = this._callback;
this._callback = null;
this._addons = null;
callback.searchFailed();
},
// Parses an add-on entry from an <addon> element
_parseAddon: function(element, known_ids) {
var app = Cc["@mozilla.org/xre/app-info;1"].
getService(Ci.nsIXULAppInfo).
QueryInterface(Ci.nsIXULRuntime);
var guidList = element.getElementsByTagName("guid");
if (guidList.length != 1)
return;
var guid = guidList[0].textContent.trim();
// Ignore add-ons already seen in the results
for (var i = 0; i < this._addons.length; i++)
if (this._addons[i].id == guid)
return;
// Ignore installed add-ons
if (known_ids.indexOf(guid) != -1)
return;
// Ignore sandboxed add-ons
var status = element.getElementsByTagName("status");
// The status element has a unique id for each status type. 4 is Public.
if (status.length != 1 || status[0].getAttribute("id") != 4)
return;
// Ignore add-ons not compatible with this OS
var osList = element.getElementsByTagName("compatible_os");
// Only the version 0 schema included compatible_os if it isn't there then
// we will see os compatibility on the install elements.
if (osList.length > 0) {
var compatible = false;
var i = 0;
while (i < osList.length && !compatible) {
var os = osList[i].textContent.trim();
if (os == "ALL" || os == app.OS) {
compatible = true;
break;
}
i++;
}
if (!compatible)
return;
}
// Ignore add-ons not compatible with this Application
compatible = false;
var tags = element.getElementsByTagName("compatible_applications");
if (tags.length != 1)
return;
var vc = Cc["@mozilla.org/xpcom/version-comparator;1"].
getService(Ci.nsIVersionComparator);
var apps = tags[0].getElementsByTagName("appID");
var i = 0;
while (i < apps.length) {
if (apps[i].textContent.trim() == app.ID) {
var parent = apps[i].parentNode;
var minversion = parent.getElementsByTagName("min_version")[0].textContent.trim();
var maxversion = parent.getElementsByTagName("max_version")[0].textContent.trim();
if ((vc.compare(minversion, app.version) > 0) ||
(vc.compare(app.version, maxversion) > 0))
return;
compatible = true;
break;
}
i++;
}
if (!compatible)
return;
var addon = new AddonSearchResult();
addon.id = guid;
addon.rating = -1;
var node = element.firstChild;
while (node) {
if (node instanceof Ci.nsIDOMElement) {
switch (node.localName) {
case "name":
case "version":
case "summary":
case "description":
case "eula":
addon[node.localName] = node.textContent.trim();
break;
case "rating":
if (node.textContent.length > 0) {
var rating = parseInt(node.textContent);
if (rating >= 0)
addon.rating = Math.min(5, rating);
}
break;
case "thumbnail":
addon.thumbnailURL = node.textContent.trim();
break;
case "icon":
addon.iconURL = node.textContent.trim();
break;
case "learnmore":
addon.homepageURL = node.textContent.trim();
break;
case "type":
// The type element has an id attribute that is the id from AMO's
// database. This doesn't match our type values to perform a mapping
if (node.getAttribute("id") == 2)
addon.type = Ci.nsIAddonSearchResult.TYPE_THEME;
else
addon.type = Ci.nsIAddonSearchResult.TYPE_EXTENSION;
break;
case "install":
// No os attribute means the xpi is compatible with any os
if (node.hasAttribute("os")) {
var os = node.getAttribute("os").toLowerCase();
// If the os is not ALL and not the current OS then ignore this xpi
if (os != "all" && os != app.OS.toLowerCase())
break;
}
addon.xpiURL = node.textContent.trim();
if (node.hasAttribute("hash"))
addon.xpiHash = node.getAttribute("hash");
break;
}
}
node = node.nextSibling;
}
// Add only if there was an xpi compatible with this os
if (addon.xpiURL)
this._addons.push(addon);
},
// Called when a single request has completed, parses out any add-ons and
// either notifies the callback or does a new request for more results
_listLoaded: function(aEvent) {
var request = aEvent.target;
var responseXML = request.responseXML;
if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
(request.status != 200 && request.status != 0)) {
this._reportFailure();
return;
}
var self = this;
AddonManager.getAllAddons(function(addons) {
var known_ids = [a.id for each(a in addons)];
var elements = responseXML.documentElement.getElementsByTagName("addon");
for (var i = 0; i < elements.length; i++) {
self._parseAddon(elements[i], known_ids);
var prefs = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch);
if (self._addons.length == self._maxResults) {
self._reportSuccess(elements.length);
return;
}
}
if (responseXML.documentElement.hasAttribute("total_results"))
self._reportSuccess(responseXML.documentElement.getAttribute("total_results"));
else
self._reportSuccess(elements.length);
});
},
// Performs a new request for results
_loadList: function(aURI) {
this._request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
this._request.open("GET", aURI, true);
this._request.overrideMimeType("text/xml");
var self = this;
this._request.onerror = function(event) { self._reportFailure(event); };
this._request.onload = function(event) { self._listLoaded(event); };
this._request.send(null);
},
classDescription: "Addon Repository",
contractID: "@mozilla.org/extensions/addon-repository;1",
classID: Components.ID("{8eaaf524-7d6d-4f7d-ae8b-9277b324008d}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonRepository])
}
function NSGetModule(aCompMgr, aFileSpec) {
return XPCOMUtils.generateModule([AddonRepository]);
}