services/sync/modules/engines/history.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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 this.EXPORTED_SYMBOLS = ['HistoryEngine', 'HistoryRec'];
     7 const Cc = Components.classes;
     8 const Ci = Components.interfaces;
     9 const Cu = Components.utils;
    10 const Cr = Components.results;
    12 const HISTORY_TTL = 5184000; // 60 days
    14 Cu.import("resource://gre/modules/PlacesUtils.jsm", this);
    15 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    16 Cu.import("resource://services-common/async.js");
    17 Cu.import("resource://gre/modules/Log.jsm");
    18 Cu.import("resource://services-sync/constants.js");
    19 Cu.import("resource://services-sync/engines.js");
    20 Cu.import("resource://services-sync/record.js");
    21 Cu.import("resource://services-sync/util.js");
    23 this.HistoryRec = function HistoryRec(collection, id) {
    24   CryptoWrapper.call(this, collection, id);
    25 }
    26 HistoryRec.prototype = {
    27   __proto__: CryptoWrapper.prototype,
    28   _logName: "Sync.Record.History",
    29   ttl: HISTORY_TTL
    30 };
    32 Utils.deferGetSet(HistoryRec, "cleartext", ["histUri", "title", "visits"]);
    35 this.HistoryEngine = function HistoryEngine(service) {
    36   SyncEngine.call(this, "History", service);
    37 }
    38 HistoryEngine.prototype = {
    39   __proto__: SyncEngine.prototype,
    40   _recordObj: HistoryRec,
    41   _storeObj: HistoryStore,
    42   _trackerObj: HistoryTracker,
    43   downloadLimit: MAX_HISTORY_DOWNLOAD,
    44   applyIncomingBatchSize: HISTORY_STORE_BATCH_SIZE
    45 };
    47 function HistoryStore(name, engine) {
    48   Store.call(this, name, engine);
    50   // Explicitly nullify our references to our cached services so we don't leak
    51   Svc.Obs.add("places-shutdown", function() {
    52     for each ([query, stmt] in Iterator(this._stmts)) {
    53       stmt.finalize();
    54     }
    55     this._stmts = {};
    56   }, this);
    57 }
    58 HistoryStore.prototype = {
    59   __proto__: Store.prototype,
    61   __asyncHistory: null,
    62   get _asyncHistory() {
    63     if (!this.__asyncHistory) {
    64       this.__asyncHistory = Cc["@mozilla.org/browser/history;1"]
    65                               .getService(Ci.mozIAsyncHistory);
    66     }
    67     return this.__asyncHistory;
    68   },
    70   _stmts: {},
    71   _getStmt: function(query) {
    72     if (query in this._stmts) {
    73       return this._stmts[query];
    74     }
    76     this._log.trace("Creating SQL statement: " + query);
    77     let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
    78                         .DBConnection;
    79     return this._stmts[query] = db.createAsyncStatement(query);
    80   },
    82   get _setGUIDStm() {
    83     return this._getStmt(
    84       "UPDATE moz_places " +
    85       "SET guid = :guid " +
    86       "WHERE url = :page_url");
    87   },
    89   // Some helper functions to handle GUIDs
    90   setGUID: function setGUID(uri, guid) {
    91     uri = uri.spec ? uri.spec : uri;
    93     if (!guid) {
    94       guid = Utils.makeGUID();
    95     }
    97     let stmt = this._setGUIDStm;
    98     stmt.params.guid = guid;
    99     stmt.params.page_url = uri;
   100     Async.querySpinningly(stmt);
   101     return guid;
   102   },
   104   get _guidStm() {
   105     return this._getStmt(
   106       "SELECT guid " +
   107       "FROM moz_places " +
   108       "WHERE url = :page_url");
   109   },
   110   _guidCols: ["guid"],
   112   GUIDForUri: function GUIDForUri(uri, create) {
   113     let stm = this._guidStm;
   114     stm.params.page_url = uri.spec ? uri.spec : uri;
   116     // Use the existing GUID if it exists
   117     let result = Async.querySpinningly(stm, this._guidCols)[0];
   118     if (result && result.guid)
   119       return result.guid;
   121     // Give the uri a GUID if it doesn't have one
   122     if (create)
   123       return this.setGUID(uri);
   124   },
   126   get _visitStm() {
   127     return this._getStmt(
   128       "/* do not warn (bug 599936) */ " +
   129       "SELECT visit_type type, visit_date date " +
   130       "FROM moz_historyvisits " +
   131       "WHERE place_id = (SELECT id FROM moz_places WHERE url = :url) " +
   132       "ORDER BY date DESC LIMIT 10");
   133   },
   134   _visitCols: ["date", "type"],
   136   get _urlStm() {
   137     return this._getStmt(
   138       "SELECT url, title, frecency " +
   139       "FROM moz_places " +
   140       "WHERE guid = :guid");
   141   },
   142   _urlCols: ["url", "title", "frecency"],
   144   get _allUrlStm() {
   145     return this._getStmt(
   146       "SELECT url " +
   147       "FROM moz_places " +
   148       "WHERE last_visit_date > :cutoff_date " +
   149       "ORDER BY frecency DESC " +
   150       "LIMIT :max_results");
   151   },
   152   _allUrlCols: ["url"],
   154   // See bug 320831 for why we use SQL here
   155   _getVisits: function HistStore__getVisits(uri) {
   156     this._visitStm.params.url = uri;
   157     return Async.querySpinningly(this._visitStm, this._visitCols);
   158   },
   160   // See bug 468732 for why we use SQL here
   161   _findURLByGUID: function HistStore__findURLByGUID(guid) {
   162     this._urlStm.params.guid = guid;
   163     return Async.querySpinningly(this._urlStm, this._urlCols)[0];
   164   },
   166   changeItemID: function HStore_changeItemID(oldID, newID) {
   167     this.setGUID(this._findURLByGUID(oldID).url, newID);
   168   },
   171   getAllIDs: function HistStore_getAllIDs() {
   172     // Only get places visited within the last 30 days (30*24*60*60*1000ms)
   173     this._allUrlStm.params.cutoff_date = (Date.now() - 2592000000) * 1000;
   174     this._allUrlStm.params.max_results = MAX_HISTORY_UPLOAD;
   176     let urls = Async.querySpinningly(this._allUrlStm, this._allUrlCols);
   177     let self = this;
   178     return urls.reduce(function(ids, item) {
   179       ids[self.GUIDForUri(item.url, true)] = item.url;
   180       return ids;
   181     }, {});
   182   },
   184   applyIncomingBatch: function applyIncomingBatch(records) {
   185     let failed = [];
   187     // Convert incoming records to mozIPlaceInfo objects. Some records can be
   188     // ignored or handled directly, so we're rewriting the array in-place.
   189     let i, k;
   190     for (i = 0, k = 0; i < records.length; i++) {
   191       let record = records[k] = records[i];
   192       let shouldApply;
   194       // This is still synchronous I/O for now.
   195       try {
   196         if (record.deleted) {
   197           // Consider using nsIBrowserHistory::removePages() here.
   198           this.remove(record);
   199           // No further processing needed. Remove it from the list.
   200           shouldApply = false;
   201         } else {
   202           shouldApply = this._recordToPlaceInfo(record);
   203         }
   204       } catch(ex) {
   205         failed.push(record.id);
   206         shouldApply = false;
   207       }
   209       if (shouldApply) {
   210         k += 1;
   211       }
   212     }
   213     records.length = k; // truncate array
   215     // Nothing to do.
   216     if (!records.length) {
   217       return failed;
   218     }
   220     let updatePlacesCallback = { 
   221       handleResult: function handleResult() {},
   222       handleError: function handleError(resultCode, placeInfo) {
   223         failed.push(placeInfo.guid);
   224       },
   225       handleCompletion: Async.makeSyncCallback()
   226     };
   227     this._asyncHistory.updatePlaces(records, updatePlacesCallback);
   228     Async.waitForSyncCallback(updatePlacesCallback.handleCompletion);
   229     return failed;
   230   },
   232   /**
   233    * Converts a Sync history record to a mozIPlaceInfo.
   234    * 
   235    * Throws if an invalid record is encountered (invalid URI, etc.),
   236    * returns true if the record is to be applied, false otherwise
   237    * (no visits to add, etc.),
   238    */
   239   _recordToPlaceInfo: function _recordToPlaceInfo(record) {
   240     // Sort out invalid URIs and ones Places just simply doesn't want.
   241     record.uri = Utils.makeURI(record.histUri);
   242     if (!record.uri) {
   243       this._log.warn("Attempted to process invalid URI, skipping.");
   244       throw "Invalid URI in record";
   245     }
   247     if (!Utils.checkGUID(record.id)) {
   248       this._log.warn("Encountered record with invalid GUID: " + record.id);
   249       return false;
   250     }
   251     record.guid = record.id;
   253     if (!PlacesUtils.history.canAddURI(record.uri)) {
   254       this._log.trace("Ignoring record " + record.id + " with URI "
   255                       + record.uri.spec + ": can't add this URI.");
   256       return false;
   257     }
   259     // We dupe visits by date and type. So an incoming visit that has
   260     // the same timestamp and type as a local one won't get applied.
   261     // To avoid creating new objects, we rewrite the query result so we
   262     // can simply check for containment below.
   263     let curVisits = this._getVisits(record.histUri);
   264     let i, k;
   265     for (i = 0; i < curVisits.length; i++) {
   266       curVisits[i] = curVisits[i].date + "," + curVisits[i].type;
   267     }
   269     // Walk through the visits, make sure we have sound data, and eliminate
   270     // dupes. The latter is done by rewriting the array in-place.
   271     for (i = 0, k = 0; i < record.visits.length; i++) {
   272       let visit = record.visits[k] = record.visits[i];
   274       if (!visit.date || typeof visit.date != "number") {
   275         this._log.warn("Encountered record with invalid visit date: "
   276                        + visit.date);
   277         throw "Visit has no date!";
   278       }
   280       if (!visit.type || !(visit.type >= PlacesUtils.history.TRANSITION_LINK &&
   281                            visit.type <= PlacesUtils.history.TRANSITION_FRAMED_LINK)) {
   282         this._log.warn("Encountered record with invalid visit type: "
   283                        + visit.type);
   284         throw "Invalid visit type!";
   285       }
   287       // Dates need to be integers.
   288       visit.date = Math.round(visit.date);
   290       if (curVisits.indexOf(visit.date + "," + visit.type) != -1) {
   291         // Visit is a dupe, don't increment 'k' so the element will be
   292         // overwritten.
   293         continue;
   294       }
   295       visit.visitDate = visit.date;
   296       visit.transitionType = visit.type;
   297       k += 1;
   298     }
   299     record.visits.length = k; // truncate array
   301     // No update if there aren't any visits to apply.
   302     // mozIAsyncHistory::updatePlaces() wants at least one visit.
   303     // In any case, the only thing we could change would be the title
   304     // and that shouldn't change without a visit.
   305     if (!record.visits.length) {
   306       this._log.trace("Ignoring record " + record.id + " with URI "
   307                       + record.uri.spec + ": no visits to add.");
   308       return false;
   309     }
   311     return true;
   312   },
   314   remove: function HistStore_remove(record) {
   315     let page = this._findURLByGUID(record.id);
   316     if (page == null) {
   317       this._log.debug("Page already removed: " + record.id);
   318       return;
   319     }
   321     let uri = Utils.makeURI(page.url);
   322     PlacesUtils.history.removePage(uri);
   323     this._log.trace("Removed page: " + [record.id, page.url, page.title]);
   324   },
   326   itemExists: function HistStore_itemExists(id) {
   327     return !!this._findURLByGUID(id);
   328   },
   330   createRecord: function createRecord(id, collection) {
   331     let foo = this._findURLByGUID(id);
   332     let record = new HistoryRec(collection, id);
   333     if (foo) {
   334       record.histUri = foo.url;
   335       record.title = foo.title;
   336       record.sortindex = foo.frecency;
   337       record.visits = this._getVisits(record.histUri);
   338     } else {
   339       record.deleted = true;
   340     }
   342     return record;
   343   },
   345   wipe: function HistStore_wipe() {
   346     PlacesUtils.history.removeAllPages();
   347   }
   348 };
   350 function HistoryTracker(name, engine) {
   351   Tracker.call(this, name, engine);
   352 }
   353 HistoryTracker.prototype = {
   354   __proto__: Tracker.prototype,
   356   startTracking: function() {
   357     this._log.info("Adding Places observer.");
   358     PlacesUtils.history.addObserver(this, true);
   359   },
   361   stopTracking: function() {
   362     this._log.info("Removing Places observer.");
   363     PlacesUtils.history.removeObserver(this);
   364   },
   366   QueryInterface: XPCOMUtils.generateQI([
   367     Ci.nsINavHistoryObserver,
   368     Ci.nsISupportsWeakReference
   369   ]),
   371   onDeleteAffectsGUID: function (uri, guid, reason, source, increment) {
   372     if (this.ignoreAll || reason == Ci.nsINavHistoryObserver.REASON_EXPIRED) {
   373       return;
   374     }
   375     this._log.trace(source + ": " + uri.spec + ", reason " + reason);
   376     if (this.addChangedID(guid)) {
   377       this.score += increment;
   378     }
   379   },
   381   onDeleteVisits: function (uri, visitTime, guid, reason) {
   382     this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteVisits", SCORE_INCREMENT_SMALL);
   383   },
   385   onDeleteURI: function (uri, guid, reason) {
   386     this.onDeleteAffectsGUID(uri, guid, reason, "onDeleteURI", SCORE_INCREMENT_XLARGE);
   387   },
   389   onVisit: function (uri, vid, time, session, referrer, trans, guid) {
   390     if (this.ignoreAll) {
   391       this._log.trace("ignoreAll: ignoring visit for " + guid);
   392       return;
   393     }
   395     this._log.trace("onVisit: " + uri.spec);
   396     if (this.addChangedID(guid)) {
   397       this.score += SCORE_INCREMENT_SMALL;
   398     }
   399   },
   401   onClearHistory: function () {
   402     this._log.trace("onClearHistory");
   403     // Note that we're going to trigger a sync, but none of the cleared
   404     // pages are tracked, so the deletions will not be propagated.
   405     // See Bug 578694.
   406     this.score += SCORE_INCREMENT_XLARGE;
   407   },
   409   onBeginUpdateBatch: function () {},
   410   onEndUpdateBatch: function () {},
   411   onPageChanged: function () {},
   412   onTitleChanged: function () {},
   413   onBeforeDeleteURI: function () {},
   414 };

mercurial