|
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/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 const Cc = Components.classes; |
|
8 const Ci = Components.interfaces; |
|
9 const Cu = Components.utils; |
|
10 |
|
11 Cu.import("resource://gre/modules/Services.jsm"); |
|
12 Cu.import("resource://gre/modules/AddonManager.jsm"); |
|
13 Cu.import("resource://gre/modules/FileUtils.jsm"); |
|
14 |
|
15 const KEY_PROFILEDIR = "ProfD"; |
|
16 const FILE_DATABASE = "addons.sqlite"; |
|
17 const LAST_DB_SCHEMA = 4; |
|
18 |
|
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"]; |
|
27 |
|
28 Cu.import("resource://gre/modules/Log.jsm"); |
|
29 const LOGGER_ID = "addons.repository.sqlmigrator"; |
|
30 |
|
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); |
|
34 |
|
35 this.EXPORTED_SYMBOLS = ["AddonRepository_SQLiteMigrator"]; |
|
36 |
|
37 |
|
38 this.AddonRepository_SQLiteMigrator = { |
|
39 |
|
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 } |
|
58 |
|
59 logger.debug("Importing addon repository from previous " + FILE_DATABASE + " storage."); |
|
60 |
|
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 }); |
|
67 |
|
68 return true; |
|
69 }, |
|
70 |
|
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; |
|
78 |
|
79 let dbfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true); |
|
80 if (!dbfile.exists()) |
|
81 return false; |
|
82 |
|
83 try { |
|
84 this.connection = Services.storage.openUnsharedDatabase(dbfile); |
|
85 } catch (e) { |
|
86 return false; |
|
87 } |
|
88 |
|
89 this.connection.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE"); |
|
90 |
|
91 // Any errors in here should rollback |
|
92 try { |
|
93 this.connection.beginTransaction(); |
|
94 |
|
95 switch (this.connection.schemaVersion) { |
|
96 case 0: |
|
97 return false; |
|
98 |
|
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 } |
|
139 |
|
140 return true; |
|
141 }, |
|
142 |
|
143 _closeConnection: function() { |
|
144 for each (let stmt in this.asyncStatementsCache) |
|
145 stmt.finalize(); |
|
146 this.asyncStatementsCache = {}; |
|
147 |
|
148 if (this.connection) |
|
149 this.connection.asyncClose(); |
|
150 |
|
151 delete this.connection; |
|
152 }, |
|
153 |
|
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 = {}; |
|
164 |
|
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 }, |
|
175 |
|
176 handleError: self.asyncErrorLogger, |
|
177 |
|
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 } |
|
184 |
|
185 getAllDevelopers(); |
|
186 } |
|
187 }); |
|
188 } |
|
189 |
|
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 } |
|
201 |
|
202 let addon = addons[addon_internal_id]; |
|
203 if (!addon.developers) |
|
204 addon.developers = []; |
|
205 |
|
206 addon.developers.push(self._makeDeveloperFromAsyncRow(row)); |
|
207 } |
|
208 }, |
|
209 |
|
210 handleError: self.asyncErrorLogger, |
|
211 |
|
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 } |
|
218 |
|
219 getAllScreenshots(); |
|
220 } |
|
221 }); |
|
222 } |
|
223 |
|
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 } |
|
235 |
|
236 let addon = addons[addon_internal_id]; |
|
237 if (!addon.screenshots) |
|
238 addon.screenshots = []; |
|
239 addon.screenshots.push(self._makeScreenshotFromAsyncRow(row)); |
|
240 } |
|
241 }, |
|
242 |
|
243 handleError: self.asyncErrorLogger, |
|
244 |
|
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 } |
|
251 |
|
252 getAllCompatOverrides(); |
|
253 } |
|
254 }); |
|
255 } |
|
256 |
|
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 } |
|
267 |
|
268 let addon = addons[addon_internal_id]; |
|
269 if (!addon.compatibilityOverrides) |
|
270 addon.compatibilityOverrides = []; |
|
271 addon.compatibilityOverrides.push(self._makeCompatOverrideFromAsyncRow(row)); |
|
272 } |
|
273 }, |
|
274 |
|
275 handleError: self.asyncErrorLogger, |
|
276 |
|
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 } |
|
283 |
|
284 getAllIcons(); |
|
285 } |
|
286 }); |
|
287 } |
|
288 |
|
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 } |
|
299 |
|
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 }, |
|
307 |
|
308 handleError: self.asyncErrorLogger, |
|
309 |
|
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 } |
|
316 |
|
317 let returnedAddons = {}; |
|
318 for each (let addon in addons) |
|
319 returnedAddons[addon.id] = addon; |
|
320 aCallback(returnedAddons); |
|
321 } |
|
322 }); |
|
323 } |
|
324 |
|
325 // Begin asynchronous process |
|
326 getAllAddons(); |
|
327 }, |
|
328 |
|
329 // A cache of statements that are used and need to be finalized on shutdown |
|
330 asyncStatementsCache: {}, |
|
331 |
|
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]; |
|
344 |
|
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 }, |
|
354 |
|
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", |
|
364 |
|
365 getAllDevelopers: "SELECT addon_internal_id, name, url FROM developer " + |
|
366 "ORDER BY addon_internal_id, num", |
|
367 |
|
368 getAllScreenshots: "SELECT addon_internal_id, url, width, height, " + |
|
369 "thumbnailURL, thumbnailWidth, thumbnailHeight, caption " + |
|
370 "FROM screenshot ORDER BY addon_internal_id, num", |
|
371 |
|
372 getAllCompatOverrides: "SELECT addon_internal_id, type, minVersion, " + |
|
373 "maxVersion, appID, appMinVersion, appMaxVersion " + |
|
374 "FROM compatibility_override " + |
|
375 "ORDER BY addon_internal_id, num", |
|
376 |
|
377 getAllIcons: "SELECT addon_internal_id, size, url FROM icon " + |
|
378 "ORDER BY addon_internal_id, size", |
|
379 }, |
|
380 |
|
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. |
|
392 |
|
393 let addon = { icons: {} }; |
|
394 |
|
395 for (let prop of PROP_SINGLE) { |
|
396 addon[prop] = aRow.getResultByName(prop) |
|
397 }; |
|
398 |
|
399 return addon; |
|
400 }, |
|
401 |
|
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 }, |
|
414 |
|
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 }, |
|
433 |
|
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 }, |
|
455 |
|
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 }, |
|
468 |
|
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 }, |
|
480 |
|
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 }, |
|
490 |
|
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 }, |
|
504 |
|
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 } |