toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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 }

mercurial