Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const Cc = Components.classes;
8 const Ci = Components.interfaces;
9 const Cu = Components.utils;
11 Cu.import("resource://gre/modules/Services.jsm");
12 Cu.import("resource://gre/modules/AddonManager.jsm");
13 Cu.import("resource://gre/modules/FileUtils.jsm");
15 const KEY_PROFILEDIR = "ProfD";
16 const FILE_DATABASE = "addons.sqlite";
17 const LAST_DB_SCHEMA = 4;
19 // Add-on properties present in the columns of the database
20 const PROP_SINGLE = ["id", "type", "name", "version", "creator", "description",
21 "fullDescription", "developerComments", "eula",
22 "homepageURL", "supportURL", "contributionURL",
23 "contributionAmount", "averageRating", "reviewCount",
24 "reviewURL", "totalDownloads", "weeklyDownloads",
25 "dailyUsers", "sourceURI", "repositoryStatus", "size",
26 "updateDate"];
28 Cu.import("resource://gre/modules/Log.jsm");
29 const LOGGER_ID = "addons.repository.sqlmigrator";
31 // Create a new logger for use by the Addons Repository SQL Migrator
32 // (Requires AddonManager.jsm)
33 let logger = Log.repository.getLogger(LOGGER_ID);
35 this.EXPORTED_SYMBOLS = ["AddonRepository_SQLiteMigrator"];
38 this.AddonRepository_SQLiteMigrator = {
40 /**
41 * Migrates data from a previous SQLite version of the
42 * database to the JSON version.
43 *
44 * @param structFunctions an object that contains functions
45 * to create the various objects used
46 * in the new JSON format
47 * @param aCallback A callback to be called when migration
48 * finishes, with the results in an array
49 * @returns bool True if a migration will happen (DB was
50 * found and succesfully opened)
51 */
52 migrate: function(aCallback) {
53 if (!this._openConnection()) {
54 this._closeConnection();
55 aCallback([]);
56 return false;
57 }
59 logger.debug("Importing addon repository from previous " + FILE_DATABASE + " storage.");
61 this._retrieveStoredData((results) => {
62 this._closeConnection();
63 let resultArray = [addon for ([,addon] of Iterator(results))];
64 logger.debug(resultArray.length + " addons imported.")
65 aCallback(resultArray);
66 });
68 return true;
69 },
71 /**
72 * Synchronously opens a new connection to the database file.
73 *
74 * @return bool Whether the DB was opened successfully.
75 */
76 _openConnection: function AD_openConnection() {
77 delete this.connection;
79 let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
80 if (!dbfile.exists())
81 return false;
83 try {
84 this.connection = Services.storage.openUnsharedDatabase(dbfile);
85 } catch (e) {
86 return false;
87 }
89 this.connection.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
91 // Any errors in here should rollback
92 try {
93 this.connection.beginTransaction();
95 switch (this.connection.schemaVersion) {
96 case 0:
97 return false;
99 case 1:
100 logger.debug("Upgrading database schema to version 2");
101 this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN width INTEGER");
102 this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN height INTEGER");
103 this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailWidth INTEGER");
104 this.connection.executeSimpleSQL("ALTER TABLE screenshot ADD COLUMN thumbnailHeight INTEGER");
105 case 2:
106 logger.debug("Upgrading database schema to version 3");
107 this.connection.createTable("compatibility_override",
108 "addon_internal_id INTEGER, " +
109 "num INTEGER, " +
110 "type TEXT, " +
111 "minVersion TEXT, " +
112 "maxVersion TEXT, " +
113 "appID TEXT, " +
114 "appMinVersion TEXT, " +
115 "appMaxVersion TEXT, " +
116 "PRIMARY KEY (addon_internal_id, num)");
117 case 3:
118 logger.debug("Upgrading database schema to version 4");
119 this.connection.createTable("icon",
120 "addon_internal_id INTEGER, " +
121 "size INTEGER, " +
122 "url TEXT, " +
123 "PRIMARY KEY (addon_internal_id, size)");
124 this._createIndices();
125 this._createTriggers();
126 this.connection.schemaVersion = LAST_DB_SCHEMA;
127 case LAST_DB_SCHEMA:
128 break;
129 default:
130 return false;
131 }
132 this.connection.commitTransaction();
133 } catch (e) {
134 logger.error("Failed to open " + FILE_DATABASE + ". Data import will not happen.", e);
135 this.logSQLError(this.connection.lastError, this.connection.lastErrorString);
136 this.connection.rollbackTransaction();
137 return false;
138 }
140 return true;
141 },
143 _closeConnection: function() {
144 for each (let stmt in this.asyncStatementsCache)
145 stmt.finalize();
146 this.asyncStatementsCache = {};
148 if (this.connection)
149 this.connection.asyncClose();
151 delete this.connection;
152 },
154 /**
155 * Asynchronously retrieve all add-ons from the database, and pass it
156 * to the specified callback
157 *
158 * @param aCallback
159 * The callback to pass the add-ons back to
160 */
161 _retrieveStoredData: function AD_retrieveStoredData(aCallback) {
162 let self = this;
163 let addons = {};
165 // Retrieve all data from the addon table
166 function getAllAddons() {
167 self.getAsyncStatement("getAllAddons").executeAsync({
168 handleResult: function getAllAddons_handleResult(aResults) {
169 let row = null;
170 while ((row = aResults.getNextRow())) {
171 let internal_id = row.getResultByName("internal_id");
172 addons[internal_id] = self._makeAddonFromAsyncRow(row);
173 }
174 },
176 handleError: self.asyncErrorLogger,
178 handleCompletion: function getAllAddons_handleCompletion(aReason) {
179 if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
180 logger.error("Error retrieving add-ons from database. Returning empty results");
181 aCallback({});
182 return;
183 }
185 getAllDevelopers();
186 }
187 });
188 }
190 // Retrieve all data from the developer table
191 function getAllDevelopers() {
192 self.getAsyncStatement("getAllDevelopers").executeAsync({
193 handleResult: function getAllDevelopers_handleResult(aResults) {
194 let row = null;
195 while ((row = aResults.getNextRow())) {
196 let addon_internal_id = row.getResultByName("addon_internal_id");
197 if (!(addon_internal_id in addons)) {
198 logger.warn("Found a developer not linked to an add-on in database");
199 continue;
200 }
202 let addon = addons[addon_internal_id];
203 if (!addon.developers)
204 addon.developers = [];
206 addon.developers.push(self._makeDeveloperFromAsyncRow(row));
207 }
208 },
210 handleError: self.asyncErrorLogger,
212 handleCompletion: function getAllDevelopers_handleCompletion(aReason) {
213 if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
214 logger.error("Error retrieving developers from database. Returning empty results");
215 aCallback({});
216 return;
217 }
219 getAllScreenshots();
220 }
221 });
222 }
224 // Retrieve all data from the screenshot table
225 function getAllScreenshots() {
226 self.getAsyncStatement("getAllScreenshots").executeAsync({
227 handleResult: function getAllScreenshots_handleResult(aResults) {
228 let row = null;
229 while ((row = aResults.getNextRow())) {
230 let addon_internal_id = row.getResultByName("addon_internal_id");
231 if (!(addon_internal_id in addons)) {
232 logger.warn("Found a screenshot not linked to an add-on in database");
233 continue;
234 }
236 let addon = addons[addon_internal_id];
237 if (!addon.screenshots)
238 addon.screenshots = [];
239 addon.screenshots.push(self._makeScreenshotFromAsyncRow(row));
240 }
241 },
243 handleError: self.asyncErrorLogger,
245 handleCompletion: function getAllScreenshots_handleCompletion(aReason) {
246 if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
247 logger.error("Error retrieving screenshots from database. Returning empty results");
248 aCallback({});
249 return;
250 }
252 getAllCompatOverrides();
253 }
254 });
255 }
257 function getAllCompatOverrides() {
258 self.getAsyncStatement("getAllCompatOverrides").executeAsync({
259 handleResult: function getAllCompatOverrides_handleResult(aResults) {
260 let row = null;
261 while ((row = aResults.getNextRow())) {
262 let addon_internal_id = row.getResultByName("addon_internal_id");
263 if (!(addon_internal_id in addons)) {
264 logger.warn("Found a compatibility override not linked to an add-on in database");
265 continue;
266 }
268 let addon = addons[addon_internal_id];
269 if (!addon.compatibilityOverrides)
270 addon.compatibilityOverrides = [];
271 addon.compatibilityOverrides.push(self._makeCompatOverrideFromAsyncRow(row));
272 }
273 },
275 handleError: self.asyncErrorLogger,
277 handleCompletion: function getAllCompatOverrides_handleCompletion(aReason) {
278 if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
279 logger.error("Error retrieving compatibility overrides from database. Returning empty results");
280 aCallback({});
281 return;
282 }
284 getAllIcons();
285 }
286 });
287 }
289 function getAllIcons() {
290 self.getAsyncStatement("getAllIcons").executeAsync({
291 handleResult: function getAllIcons_handleResult(aResults) {
292 let row = null;
293 while ((row = aResults.getNextRow())) {
294 let addon_internal_id = row.getResultByName("addon_internal_id");
295 if (!(addon_internal_id in addons)) {
296 logger.warn("Found an icon not linked to an add-on in database");
297 continue;
298 }
300 let addon = addons[addon_internal_id];
301 let { size, url } = self._makeIconFromAsyncRow(row);
302 addon.icons[size] = url;
303 if (size == 32)
304 addon.iconURL = url;
305 }
306 },
308 handleError: self.asyncErrorLogger,
310 handleCompletion: function getAllIcons_handleCompletion(aReason) {
311 if (aReason != Ci.mozIStorageStatementCallback.REASON_FINISHED) {
312 logger.error("Error retrieving icons from database. Returning empty results");
313 aCallback({});
314 return;
315 }
317 let returnedAddons = {};
318 for each (let addon in addons)
319 returnedAddons[addon.id] = addon;
320 aCallback(returnedAddons);
321 }
322 });
323 }
325 // Begin asynchronous process
326 getAllAddons();
327 },
329 // A cache of statements that are used and need to be finalized on shutdown
330 asyncStatementsCache: {},
332 /**
333 * Gets a cached async statement or creates a new statement if it doesn't
334 * already exist.
335 *
336 * @param aKey
337 * A unique key to reference the statement
338 * @return a mozIStorageAsyncStatement for the SQL corresponding to the
339 * unique key
340 */
341 getAsyncStatement: function AD_getAsyncStatement(aKey) {
342 if (aKey in this.asyncStatementsCache)
343 return this.asyncStatementsCache[aKey];
345 let sql = this.queries[aKey];
346 try {
347 return this.asyncStatementsCache[aKey] = this.connection.createAsyncStatement(sql);
348 } catch (e) {
349 logger.error("Error creating statement " + aKey + " (" + sql + ")");
350 throw Components.Exception("Error creating statement " + aKey + " (" + sql + "): " + e,
351 e.result);
352 }
353 },
355 // The queries used by the database
356 queries: {
357 getAllAddons: "SELECT internal_id, id, type, name, version, " +
358 "creator, creatorURL, description, fullDescription, " +
359 "developerComments, eula, homepageURL, supportURL, " +
360 "contributionURL, contributionAmount, averageRating, " +
361 "reviewCount, reviewURL, totalDownloads, weeklyDownloads, " +
362 "dailyUsers, sourceURI, repositoryStatus, size, updateDate " +
363 "FROM addon",
365 getAllDevelopers: "SELECT addon_internal_id, name, url FROM developer " +
366 "ORDER BY addon_internal_id, num",
368 getAllScreenshots: "SELECT addon_internal_id, url, width, height, " +
369 "thumbnailURL, thumbnailWidth, thumbnailHeight, caption " +
370 "FROM screenshot ORDER BY addon_internal_id, num",
372 getAllCompatOverrides: "SELECT addon_internal_id, type, minVersion, " +
373 "maxVersion, appID, appMinVersion, appMaxVersion " +
374 "FROM compatibility_override " +
375 "ORDER BY addon_internal_id, num",
377 getAllIcons: "SELECT addon_internal_id, size, url FROM icon " +
378 "ORDER BY addon_internal_id, size",
379 },
381 /**
382 * Make add-on structure from an asynchronous row.
383 *
384 * @param aRow
385 * The asynchronous row to use
386 * @return The created add-on
387 */
388 _makeAddonFromAsyncRow: function AD__makeAddonFromAsyncRow(aRow) {
389 // This is intentionally not an AddonSearchResult object in order
390 // to allow AddonDatabase._parseAddon to parse it, same as if it
391 // was read from the JSON database.
393 let addon = { icons: {} };
395 for (let prop of PROP_SINGLE) {
396 addon[prop] = aRow.getResultByName(prop)
397 };
399 return addon;
400 },
402 /**
403 * Make a developer from an asynchronous row
404 *
405 * @param aRow
406 * The asynchronous row to use
407 * @return The created developer
408 */
409 _makeDeveloperFromAsyncRow: function AD__makeDeveloperFromAsyncRow(aRow) {
410 let name = aRow.getResultByName("name");
411 let url = aRow.getResultByName("url")
412 return new AddonManagerPrivate.AddonAuthor(name, url);
413 },
415 /**
416 * Make a screenshot from an asynchronous row
417 *
418 * @param aRow
419 * The asynchronous row to use
420 * @return The created screenshot
421 */
422 _makeScreenshotFromAsyncRow: function AD__makeScreenshotFromAsyncRow(aRow) {
423 let url = aRow.getResultByName("url");
424 let width = aRow.getResultByName("width");
425 let height = aRow.getResultByName("height");
426 let thumbnailURL = aRow.getResultByName("thumbnailURL");
427 let thumbnailWidth = aRow.getResultByName("thumbnailWidth");
428 let thumbnailHeight = aRow.getResultByName("thumbnailHeight");
429 let caption = aRow.getResultByName("caption");
430 return new AddonManagerPrivate.AddonScreenshot(url, width, height, thumbnailURL,
431 thumbnailWidth, thumbnailHeight, caption);
432 },
434 /**
435 * Make a CompatibilityOverride from an asynchronous row
436 *
437 * @param aRow
438 * The asynchronous row to use
439 * @return The created CompatibilityOverride
440 */
441 _makeCompatOverrideFromAsyncRow: function AD_makeCompatOverrideFromAsyncRow(aRow) {
442 let type = aRow.getResultByName("type");
443 let minVersion = aRow.getResultByName("minVersion");
444 let maxVersion = aRow.getResultByName("maxVersion");
445 let appID = aRow.getResultByName("appID");
446 let appMinVersion = aRow.getResultByName("appMinVersion");
447 let appMaxVersion = aRow.getResultByName("appMaxVersion");
448 return new AddonManagerPrivate.AddonCompatibilityOverride(type,
449 minVersion,
450 maxVersion,
451 appID,
452 appMinVersion,
453 appMaxVersion);
454 },
456 /**
457 * Make an icon from an asynchronous row
458 *
459 * @param aRow
460 * The asynchronous row to use
461 * @return An object containing the size and URL of the icon
462 */
463 _makeIconFromAsyncRow: function AD_makeIconFromAsyncRow(aRow) {
464 let size = aRow.getResultByName("size");
465 let url = aRow.getResultByName("url");
466 return { size: size, url: url };
467 },
469 /**
470 * A helper function to log an SQL error.
471 *
472 * @param aError
473 * The storage error code associated with the error
474 * @param aErrorString
475 * An error message
476 */
477 logSQLError: function AD_logSQLError(aError, aErrorString) {
478 logger.error("SQL error " + aError + ": " + aErrorString);
479 },
481 /**
482 * A helper function to log any errors that occur during async statements.
483 *
484 * @param aError
485 * A mozIStorageError to log
486 */
487 asyncErrorLogger: function AD_asyncErrorLogger(aError) {
488 logger.error("Async SQL error " + aError.result + ": " + aError.message);
489 },
491 /**
492 * Synchronously creates the triggers in the database.
493 */
494 _createTriggers: function AD__createTriggers() {
495 this.connection.executeSimpleSQL("DROP TRIGGER IF EXISTS delete_addon");
496 this.connection.executeSimpleSQL("CREATE TRIGGER delete_addon AFTER DELETE " +
497 "ON addon BEGIN " +
498 "DELETE FROM developer WHERE addon_internal_id=old.internal_id; " +
499 "DELETE FROM screenshot WHERE addon_internal_id=old.internal_id; " +
500 "DELETE FROM compatibility_override WHERE addon_internal_id=old.internal_id; " +
501 "DELETE FROM icon WHERE addon_internal_id=old.internal_id; " +
502 "END");
503 },
505 /**
506 * Synchronously creates the indices in the database.
507 */
508 _createIndices: function AD__createIndices() {
509 this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS developer_idx " +
510 "ON developer (addon_internal_id)");
511 this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS screenshot_idx " +
512 "ON screenshot (addon_internal_id)");
513 this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS compatibility_override_idx " +
514 "ON compatibility_override (addon_internal_id)");
515 this.connection.executeSimpleSQL("CREATE INDEX IF NOT EXISTS icon_idx " +
516 "ON icon (addon_internal_id)");
517 }
518 }