зеркало из https://github.com/mozilla/gecko-dev.git
522 строки
19 KiB
JavaScript
522 строки
19 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
|
|
Components.utils.import("resource://gre/modules/Services.jsm");
|
|
Components.utils.import("resource://gre/modules/AddonManager.jsm");
|
|
Components.utils.import("resource://gre/modules/FileUtils.jsm");
|
|
|
|
const KEY_PROFILEDIR = "ProfD";
|
|
const FILE_DATABASE = "addons.sqlite";
|
|
const LAST_DB_SCHEMA = 4;
|
|
|
|
// Add-on properties present in the columns of the database
|
|
const PROP_SINGLE = ["id", "type", "name", "version", "creator", "description",
|
|
"fullDescription", "developerComments", "eula",
|
|
"homepageURL", "supportURL", "contributionURL",
|
|
"contributionAmount", "averageRating", "reviewCount",
|
|
"reviewURL", "totalDownloads", "weeklyDownloads",
|
|
"dailyUsers", "sourceURI", "repositoryStatus", "size",
|
|
"updateDate"];
|
|
|
|
|
|
["LOG", "WARN", "ERROR"].forEach(function(aName) {
|
|
this.__defineGetter__(aName, function logFuncGetter() {
|
|
Components.utils.import("resource://gre/modules/AddonLogging.jsm");
|
|
|
|
LogManager.getLogger("addons.repository.sqlmigrator", this);
|
|
return this[aName];
|
|
});
|
|
}, this);
|
|
|
|
|
|
this.EXPORTED_SYMBOLS = ["AddonRepository_SQLiteMigrator"];
|
|
|
|
|
|
this.AddonRepository_SQLiteMigrator = {
|
|
|
|
/**
|
|
* Migrates data from a previous SQLite version of the
|
|
* database to the JSON version.
|
|
*
|
|
* @param structFunctions an object that contains functions
|
|
* to create the various objects used
|
|
* in the new JSON format
|
|
* @param aCallback A callback to be called when migration
|
|
* finishes, with the results in an array
|
|
* @returns bool True if a migration will happen (DB was
|
|
* found and succesfully opened)
|
|
*/
|
|
migrate: function(aCallback) {
|
|
if (!this._openConnection()) {
|
|
this._closeConnection();
|
|
aCallback([]);
|
|
return false;
|
|
}
|
|
|
|
LOG("Importing addon repository from previous " + FILE_DATABASE + " storage.");
|
|
|
|
this._retrieveStoredData((results) => {
|
|
this._closeConnection();
|
|
let resultArray = [addon for ([,addon] of Iterator(results))];
|
|
LOG(resultArray.length + " addons imported.")
|
|
aCallback(resultArray);
|
|
});
|
|
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Synchronously opens a new connection to the database file.
|
|
*
|
|
* @return bool Whether the DB was opened successfully.
|
|
*/
|
|
_openConnection: function AD_openConnection() {
|
|
delete this.connection;
|
|
|
|
let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
|
|
if (!dbfile.exists())
|
|
return false;
|
|
|
|
try {
|
|
this.connection = Services.storage.openUnsharedDatabase(dbfile);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
|
|
this.connection.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
|
|
|
|
// Any errors in here should rollback
|
|
try {
|
|
this.connection.beginTransaction();
|
|
|
|
switch (this.connection.schemaVersion) {
|
|
case 0:
|
|
return false;
|
|
|
|
case 1:
|
|
LOG("Upgrading database schema to version 2");
|
|
this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN width INTEGER");
|
|
this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN height INTEGER");
|
|
this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailWidth INTEGER");
|
|
this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailHeight INTEGER");
|
|
case 2:
|
|
LOG("Upgrading database schema to version 3");
|
|
this.connection.createTable("compatibility_override",
|
|
"addon_internal_id INTEGER, " +
|
|
"num INTEGER, " +
|
|
"type TEXT, " +
|
|
"minVersion TEXT, " +
|
|
"maxVersion TEXT, " +
|
|
"appID TEXT, " +
|
|
"appMinVersion TEXT, " +
|
|
"appMaxVersion TEXT, " +
|
|
"PRIMARY KEY (addon_internal_id, num)");
|
|
case 3:
|
|
LOG("Upgrading database schema to version 4");
|
|
this.connection.createTable("icon",
|
|
"addon_internal_id INTEGER, " +
|
|
"size INTEGER, " +
|
|
"url TEXT, " +
|
|
"PRIMARY KEY (addon_internal_id, size)");
|
|
this._createIndices();
|
|
this._createTriggers();
|
|
this.connection.schemaVersion = LAST_DB_SCHEMA;
|
|
case LAST_DB_SCHEMA:
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
this.connection.commitTransaction();
|
|
} catch (e) {
|
|
ERROR("Failed to open " + FILE_DATABASE + ". Data import will not happen.", e);
|
|
this.logSQLError(this.connection.lastError, this.connection.lastErrorString);
|
|
this.connection.rollbackTransaction();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
_closeConnection: function() {
|
|
for each (let stmt in this.asyncStatementsCache)
|
|
stmt.finalize();
|
|
this.asyncStatementsCache = {};
|
|
|
|
if (this.connection)
|
|
this.connection.asyncClose();
|
|
|
|
delete this.connection;
|
|
},
|
|
|
|
/**
|
|
* Asynchronously retrieve all add-ons from the database, and pass it
|
|
* to the specified callback
|
|
*
|
|
* @param aCallback
|
|
* The callback to pass the add-ons back to
|
|
*/
|
|
_retrieveStoredData: function AD_retrieveStoredData(aCallback) {
|
|
let self = this;
|
|
let addons = {};
|
|
|
|
// Retrieve all data from the addon table
|
|
function getAllAddons() {
|
|
self.getAsyncStatement("getAllAddons").executeAsync({
|
|
handleResult: function getAllAddons_handleResult(aResults) {
|
|
let row = null;
|
|
while ((row = aResults.getNextRow())) {
|
|
let internal_id = row.getResultByName("internal_id");
|
|
addons[internal_id] = self._makeAddonFromAsyncRow(row);
|
|
}
|
|
},
|
|
|
|
handleError: self.asyncErrorLogger,
|
|
|
|
handleCompletion: function getAllAddons_handleCompletion(aReason) {
|
|
if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
|
ERROR("Error retrieving add-ons from database. Returning empty results");
|
|
aCallback({});
|
|
return;
|
|
}
|
|
|
|
getAllDevelopers();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Retrieve all data from the developer table
|
|
function getAllDevelopers() {
|
|
self.getAsyncStatement("getAllDevelopers").executeAsync({
|
|
handleResult: function getAllDevelopers_handleResult(aResults) {
|
|
let row = null;
|
|
while ((row = aResults.getNextRow())) {
|
|
let addon_internal_id = row.getResultByName("addon_internal_id");
|
|
if (!(addon_internal_id in addons)) {
|
|
WARN("Found a developer not linked to an add-on in database");
|
|
continue;
|
|
}
|
|
|
|
let addon = addons[addon_internal_id];
|
|
if (!addon.developers)
|
|
addon.developers = [];
|
|
|
|
addon.developers.push(self._makeDeveloperFromAsyncRow(row));
|
|
}
|
|
},
|
|
|
|
handleError: self.asyncErrorLogger,
|
|
|
|
handleCompletion: function getAllDevelopers_handleCompletion(aReason) {
|
|
if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
|
ERROR("Error retrieving developers from database. Returning empty results");
|
|
aCallback({});
|
|
return;
|
|
}
|
|
|
|
getAllScreenshots();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Retrieve all data from the screenshot table
|
|
function getAllScreenshots() {
|
|
self.getAsyncStatement("getAllScreenshots").executeAsync({
|
|
handleResult: function getAllScreenshots_handleResult(aResults) {
|
|
let row = null;
|
|
while ((row = aResults.getNextRow())) {
|
|
let addon_internal_id = row.getResultByName("addon_internal_id");
|
|
if (!(addon_internal_id in addons)) {
|
|
WARN("Found a screenshot not linked to an add-on in database");
|
|
continue;
|
|
}
|
|
|
|
let addon = addons[addon_internal_id];
|
|
if (!addon.screenshots)
|
|
addon.screenshots = [];
|
|
addon.screenshots.push(self._makeScreenshotFromAsyncRow(row));
|
|
}
|
|
},
|
|
|
|
handleError: self.asyncErrorLogger,
|
|
|
|
handleCompletion: function getAllScreenshots_handleCompletion(aReason) {
|
|
if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
|
ERROR("Error retrieving screenshots from database. Returning empty results");
|
|
aCallback({});
|
|
return;
|
|
}
|
|
|
|
getAllCompatOverrides();
|
|
}
|
|
});
|
|
}
|
|
|
|
function getAllCompatOverrides() {
|
|
self.getAsyncStatement("getAllCompatOverrides").executeAsync({
|
|
handleResult: function getAllCompatOverrides_handleResult(aResults) {
|
|
let row = null;
|
|
while ((row = aResults.getNextRow())) {
|
|
let addon_internal_id = row.getResultByName("addon_internal_id");
|
|
if (!(addon_internal_id in addons)) {
|
|
WARN("Found a compatibility override not linked to an add-on in database");
|
|
continue;
|
|
}
|
|
|
|
let addon = addons[addon_internal_id];
|
|
if (!addon.compatibilityOverrides)
|
|
addon.compatibilityOverrides = [];
|
|
addon.compatibilityOverrides.push(self._makeCompatOverrideFromAsyncRow(row));
|
|
}
|
|
},
|
|
|
|
handleError: self.asyncErrorLogger,
|
|
|
|
handleCompletion: function getAllCompatOverrides_handleCompletion(aReason) {
|
|
if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
|
ERROR("Error retrieving compatibility overrides from database. Returning empty results");
|
|
aCallback({});
|
|
return;
|
|
}
|
|
|
|
getAllIcons();
|
|
}
|
|
});
|
|
}
|
|
|
|
function getAllIcons() {
|
|
self.getAsyncStatement("getAllIcons").executeAsync({
|
|
handleResult: function getAllIcons_handleResult(aResults) {
|
|
let row = null;
|
|
while ((row = aResults.getNextRow())) {
|
|
let addon_internal_id = row.getResultByName("addon_internal_id");
|
|
if (!(addon_internal_id in addons)) {
|
|
WARN("Found an icon not linked to an add-on in database");
|
|
continue;
|
|
}
|
|
|
|
let addon = addons[addon_internal_id];
|
|
let { size, url } = self._makeIconFromAsyncRow(row);
|
|
addon.icons[size] = url;
|
|
if (size == 32)
|
|
addon.iconURL = url;
|
|
}
|
|
},
|
|
|
|
handleError: self.asyncErrorLogger,
|
|
|
|
handleCompletion: function getAllIcons_handleCompletion(aReason) {
|
|
if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
|
|
ERROR("Error retrieving icons from database. Returning empty results");
|
|
aCallback({});
|
|
return;
|
|
}
|
|
|
|
let returnedAddons = {};
|
|
for each (let addon in addons)
|
|
returnedAddons[addon.id] = addon;
|
|
aCallback(returnedAddons);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Begin asynchronous process
|
|
getAllAddons();
|
|
},
|
|
|
|
// A cache of statements that are used and need to be finalized on shutdown
|
|
asyncStatementsCache: {},
|
|
|
|
/**
|
|
* Gets a cached async statement or creates a new statement if it doesn't
|
|
* already exist.
|
|
*
|
|
* @param aKey
|
|
* A unique key to reference the statement
|
|
* @return a mozIStorageAsyncStatement for the SQL corresponding to the
|
|
* unique key
|
|
*/
|
|
getAsyncStatement: function AD_getAsyncStatement(aKey) {
|
|
if (aKey in this.asyncStatementsCache)
|
|
return this.asyncStatementsCache[aKey];
|
|
|
|
let sql = this.queries[aKey];
|
|
try {
|
|
return this.asyncStatementsCache[aKey] = this.connection.createAsyncStatement(sql);
|
|
} catch (e) {
|
|
ERROR("Error creating statement " + aKey + " (" + sql + ")");
|
|
throw Components.Exception("Error creating statement " + aKey + " (" + sql + "): " + e,
|
|
e.result);
|
|
}
|
|
},
|
|
|
|
// The queries used by the database
|
|
queries: {
|
|
getAllAddons: "SELECT internal_id, id, type, name, version, " +
|
|
"creator, creatorURL, description, fullDescription, " +
|
|
"developerComments, eula, homepageURL, supportURL, " +
|
|
"contributionURL, contributionAmount, averageRating, " +
|
|
"reviewCount, reviewURL, totalDownloads, weeklyDownloads, " +
|
|
"dailyUsers, sourceURI, repositoryStatus, size, updateDate " +
|
|
"FROM addon",
|
|
|
|
getAllDevelopers: "SELECT addon_internal_id, name, url FROM developer " +
|
|
"ORDER BY addon_internal_id, num",
|
|
|
|
getAllScreenshots: "SELECT addon_internal_id, url, width, height, " +
|
|
"thumbnailURL, thumbnailWidth, thumbnailHeight, caption " +
|
|
"FROM screenshot ORDER BY addon_internal_id, num",
|
|
|
|
getAllCompatOverrides: "SELECT addon_internal_id, type, minVersion, " +
|
|
"maxVersion, appID, appMinVersion, appMaxVersion " +
|
|
"FROM compatibility_override " +
|
|
"ORDER BY addon_internal_id, num",
|
|
|
|
getAllIcons: "SELECT addon_internal_id, size, url FROM icon " +
|
|
"ORDER BY addon_internal_id, size",
|
|
},
|
|
|
|
/**
|
|
* Make add-on structure from an asynchronous row.
|
|
*
|
|
* @param aRow
|
|
* The asynchronous row to use
|
|
* @return The created add-on
|
|
*/
|
|
_makeAddonFromAsyncRow: function AD__makeAddonFromAsyncRow(aRow) {
|
|
// This is intentionally not an AddonSearchResult object in order
|
|
// to allow AddonDatabase._parseAddon to parse it, same as if it
|
|
// was read from the JSON database.
|
|
|
|
let addon = { icons: {} };
|
|
|
|
for (let prop of PROP_SINGLE) {
|
|
addon[prop] = aRow.getResultByName(prop)
|
|
};
|
|
|
|
return addon;
|
|
},
|
|
|
|
/**
|
|
* Make a developer from an asynchronous row
|
|
*
|
|
* @param aRow
|
|
* The asynchronous row to use
|
|
* @return The created developer
|
|
*/
|
|
_makeDeveloperFromAsyncRow: function AD__makeDeveloperFromAsyncRow(aRow) {
|
|
let name = aRow.getResultByName("name");
|
|
let url = aRow.getResultByName("url")
|
|
return new AddonManagerPrivate.AddonAuthor(name, url);
|
|
},
|
|
|
|
/**
|
|
* Make a screenshot from an asynchronous row
|
|
*
|
|
* @param aRow
|
|
* The asynchronous row to use
|
|
* @return The created screenshot
|
|
*/
|
|
_makeScreenshotFromAsyncRow: function AD__makeScreenshotFromAsyncRow(aRow) {
|
|
let url = aRow.getResultByName("url");
|
|
let width = aRow.getResultByName("width");
|
|
let height = aRow.getResultByName("height");
|
|
let thumbnailURL = aRow.getResultByName("thumbnailURL");
|
|
let thumbnailWidth = aRow.getResultByName("thumbnailWidth");
|
|
let thumbnailHeight = aRow.getResultByName("thumbnailHeight");
|
|
let caption = aRow.getResultByName("caption");
|
|
return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL,
|
|
thumbnailWidth, thumbnailHeight, caption);
|
|
},
|
|
|
|
/**
|
|
* Make a CompatibilityOverride from an asynchronous row
|
|
*
|
|
* @param aRow
|
|
* The asynchronous row to use
|
|
* @return The created CompatibilityOverride
|
|
*/
|
|
_makeCompatOverrideFromAsyncRow: function AD_makeCompatOverrideFromAsyncRow(aRow) {
|
|
let type = aRow.getResultByName("type");
|
|
let minVersion = aRow.getResultByName("minVersion");
|
|
let maxVersion = aRow.getResultByName("maxVersion");
|
|
let appID = aRow.getResultByName("appID");
|
|
let appMinVersion = aRow.getResultByName("appMinVersion");
|
|
let appMaxVersion = aRow.getResultByName("appMaxVersion");
|
|
return new AddonManagerPrivate.AddonCompatibilityOverride(type,
|
|
minVersion,
|
|
maxVersion,
|
|
appID,
|
|
appMinVersion,
|
|
appMaxVersion);
|
|
},
|
|
|
|
/**
|
|
* Make an icon from an asynchronous row
|
|
*
|
|
* @param aRow
|
|
* The asynchronous row to use
|
|
* @return An object containing the size and URL of the icon
|
|
*/
|
|
_makeIconFromAsyncRow: function AD_makeIconFromAsyncRow(aRow) {
|
|
let size = aRow.getResultByName("size");
|
|
let url = aRow.getResultByName("url");
|
|
return { size: size, url: url };
|
|
},
|
|
|
|
/**
|
|
* A helper function to log an SQL error.
|
|
*
|
|
* @param aError
|
|
* The storage error code associated with the error
|
|
* @param aErrorString
|
|
* An error message
|
|
*/
|
|
logSQLError: function AD_logSQLError(aError, aErrorString) {
|
|
ERROR("SQL error " + aError + ": " + aErrorString);
|
|
},
|
|
|
|
/**
|
|
* A helper function to log any errors that occur during async statements.
|
|
*
|
|
* @param aError
|
|
* A mozIStorageError to log
|
|
*/
|
|
asyncErrorLogger: function AD_asyncErrorLogger(aError) {
|
|
ERROR("Async SQL error " + aError.result + ": " + aError.message);
|
|
},
|
|
|
|
/**
|
|
* Synchronously creates the triggers in the database.
|
|
*/
|
|
_createTriggers: function AD__createTriggers() {
|
|
this.connection.executeSimpleSQL("DROP TRIGGER IF EXISTS delete_addon");
|
|
this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon AFTER DELETE " +
|
|
"ON addon BEGIN " +
|
|
"DELETE FROM developer WHERE addon_internal_id=old.internal_id; " +
|
|
"DELETE FROM screenshot WHERE addon_internal_id=old.internal_id; " +
|
|
"DELETE FROM compatibility_override WHERE addon_internal_id=old.internal_id; " +
|
|
"DELETE FROM icon WHERE addon_internal_id=old.internal_id; " +
|
|
"END");
|
|
},
|
|
|
|
/**
|
|
* Synchronously creates the indices in the database.
|
|
*/
|
|
_createIndices: function AD__createIndices() {
|
|
this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS developer_idx " +
|
|
"ON developer (addon_internal_id)");
|
|
this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS screenshot_idx " +
|
|
"ON screenshot (addon_internal_id)");
|
|
this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS compatibility_override_idx " +
|
|
"ON compatibility_override (addon_internal_id)");
|
|
this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS icon_idx " +
|
|
"ON icon (addon_internal_id)");
|
|
}
|
|
}
|